cscg24-guacamole

CSCG 2024 Challenge 'Guacamole Mashup'
git clone https://git.sinitax.com/sinitax/cscg24-guacamole
Log | Files | Refs | sfeed.txt

guacamole-common.js (509773B)


      1/*
      2 * Licensed to the Apache Software Foundation (ASF) under one
      3 * or more contributor license agreements.  See the NOTICE file
      4 * distributed with this work for additional information
      5 * regarding copyright ownership.  The ASF licenses this file
      6 * to you under the Apache License, Version 2.0 (the
      7 * "License"); you may not use this file except in compliance
      8 * with the License.  You may obtain a copy of the License at
      9 *
     10 *   http://www.apache.org/licenses/LICENSE-2.0
     11 *
     12 * Unless required by applicable law or agreed to in writing,
     13 * software distributed under the License is distributed on an
     14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     15 * KIND, either express or implied.  See the License for the
     16 * specific language governing permissions and limitations
     17 * under the License.
     18 */
     19
     20var Guacamole = Guacamole || {};
     21
     22/**
     23 * A reader which automatically handles the given input stream, returning
     24 * strictly received packets as array buffers. Note that this object will
     25 * overwrite any installed event handlers on the given Guacamole.InputStream.
     26 * 
     27 * @constructor
     28 * @param {!Guacamole.InputStream} stream
     29 *     The stream that data will be read from.
     30 */
     31Guacamole.ArrayBufferReader = function(stream) {
     32
     33    /**
     34     * Reference to this Guacamole.InputStream.
     35     * @private
     36     */
     37    var guac_reader = this;
     38
     39    // Receive blobs as array buffers
     40    stream.onblob = function(data) {
     41
     42        // Convert to ArrayBuffer
     43        var binary = window.atob(data);
     44        var arrayBuffer = new ArrayBuffer(binary.length);
     45        var bufferView = new Uint8Array(arrayBuffer);
     46
     47        for (var i=0; i<binary.length; i++)
     48            bufferView[i] = binary.charCodeAt(i);
     49
     50        // Call handler, if present
     51        if (guac_reader.ondata)
     52            guac_reader.ondata(arrayBuffer);
     53
     54    };
     55
     56    // Simply call onend when end received
     57    stream.onend = function() {
     58        if (guac_reader.onend)
     59            guac_reader.onend();
     60    };
     61
     62    /**
     63     * Fired once for every blob of data received.
     64     * 
     65     * @event
     66     * @param {!ArrayBuffer} buffer
     67     *     The data packet received.
     68     */
     69    this.ondata = null;
     70
     71    /**
     72     * Fired once this stream is finished and no further data will be written.
     73     * @event
     74     */
     75    this.onend = null;
     76
     77};/*
     78 * Licensed to the Apache Software Foundation (ASF) under one
     79 * or more contributor license agreements.  See the NOTICE file
     80 * distributed with this work for additional information
     81 * regarding copyright ownership.  The ASF licenses this file
     82 * to you under the Apache License, Version 2.0 (the
     83 * "License"); you may not use this file except in compliance
     84 * with the License.  You may obtain a copy of the License at
     85 *
     86 *   http://www.apache.org/licenses/LICENSE-2.0
     87 *
     88 * Unless required by applicable law or agreed to in writing,
     89 * software distributed under the License is distributed on an
     90 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     91 * KIND, either express or implied.  See the License for the
     92 * specific language governing permissions and limitations
     93 * under the License.
     94 */
     95
     96var Guacamole = Guacamole || {};
     97
     98/**
     99 * A writer which automatically writes to the given output stream with arbitrary
    100 * binary data, supplied as ArrayBuffers.
    101 * 
    102 * @constructor
    103 * @param {!Guacamole.OutputStream} stream
    104 *     The stream that data will be written to.
    105 */
    106Guacamole.ArrayBufferWriter = function(stream) {
    107
    108    /**
    109     * Reference to this Guacamole.StringWriter.
    110     *
    111     * @private
    112     * @type {!Guacamole.ArrayBufferWriter}
    113     */
    114    var guac_writer = this;
    115
    116    // Simply call onack for acknowledgements
    117    stream.onack = function(status) {
    118        if (guac_writer.onack)
    119            guac_writer.onack(status);
    120    };
    121
    122    /**
    123     * Encodes the given data as base64, sending it as a blob. The data must
    124     * be small enough to fit into a single blob instruction.
    125     * 
    126     * @private
    127     * @param {!Uint8Array} bytes
    128     *     The data to send.
    129     */
    130    function __send_blob(bytes) {
    131
    132        var binary = "";
    133
    134        // Produce binary string from bytes in buffer
    135        for (var i=0; i<bytes.byteLength; i++)
    136            binary += String.fromCharCode(bytes[i]);
    137
    138        // Send as base64
    139        stream.sendBlob(window.btoa(binary));
    140
    141    }
    142
    143    /**
    144     * The maximum length of any blob sent by this Guacamole.ArrayBufferWriter,
    145     * in bytes. Data sent via
    146     * [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds
    147     * this length will be split into multiple blobs. As the Guacamole protocol
    148     * limits the maximum size of any instruction or instruction element to
    149     * 8192 bytes, and the contents of blobs will be base64-encoded, this value
    150     * should only be increased with extreme caution.
    151     *
    152     * @type {!number}
    153     * @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH}
    154     */
    155    this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH;
    156
    157    /**
    158     * Sends the given data.
    159     * 
    160     * @param {!(ArrayBuffer|TypedArray)} data
    161     *     The data to send.
    162     */
    163    this.sendData = function(data) {
    164
    165        var bytes = new Uint8Array(data);
    166
    167        // If small enough to fit into single instruction, send as-is
    168        if (bytes.length <= guac_writer.blobLength)
    169            __send_blob(bytes);
    170
    171        // Otherwise, send as multiple instructions
    172        else {
    173            for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength)
    174                __send_blob(bytes.subarray(offset, offset + guac_writer.blobLength));
    175        }
    176
    177    };
    178
    179    /**
    180     * Signals that no further text will be sent, effectively closing the
    181     * stream.
    182     */
    183    this.sendEnd = function() {
    184        stream.sendEnd();
    185    };
    186
    187    /**
    188     * Fired for received data, if acknowledged by the server.
    189     * @event
    190     * @param {!Guacamole.Status} status
    191     *     The status of the operation.
    192     */
    193    this.onack = null;
    194
    195};
    196
    197/**
    198 * The default maximum blob length for new Guacamole.ArrayBufferWriter
    199 * instances.
    200 *
    201 * @constant
    202 * @type {!number}
    203 */
    204Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048;
    205/*
    206 * Licensed to the Apache Software Foundation (ASF) under one
    207 * or more contributor license agreements.  See the NOTICE file
    208 * distributed with this work for additional information
    209 * regarding copyright ownership.  The ASF licenses this file
    210 * to you under the Apache License, Version 2.0 (the
    211 * "License"); you may not use this file except in compliance
    212 * with the License.  You may obtain a copy of the License at
    213 *
    214 *   http://www.apache.org/licenses/LICENSE-2.0
    215 *
    216 * Unless required by applicable law or agreed to in writing,
    217 * software distributed under the License is distributed on an
    218 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    219 * KIND, either express or implied.  See the License for the
    220 * specific language governing permissions and limitations
    221 * under the License.
    222 */
    223
    224var Guacamole = Guacamole || {};
    225
    226/**
    227 * Maintains a singleton instance of the Web Audio API AudioContext class,
    228 * instantiating the AudioContext only in response to the first call to
    229 * getAudioContext(), and only if no existing AudioContext instance has been
    230 * provided via the singleton property. Subsequent calls to getAudioContext()
    231 * will return the same instance.
    232 *
    233 * @namespace
    234 */
    235Guacamole.AudioContextFactory = {
    236
    237    /**
    238     * A singleton instance of a Web Audio API AudioContext object, or null if
    239     * no instance has yes been created. This property may be manually set if
    240     * you wish to supply your own AudioContext instance, but care must be
    241     * taken to do so as early as possible. Assignments to this property will
    242     * not retroactively affect the value returned by previous calls to
    243     * getAudioContext().
    244     *
    245     * @type {AudioContext}
    246     */
    247    'singleton' : null,
    248
    249    /**
    250     * Returns a singleton instance of a Web Audio API AudioContext object.
    251     *
    252     * @return {AudioContext}
    253     *     A singleton instance of a Web Audio API AudioContext object, or null
    254     *     if the Web Audio API is not supported.
    255     */
    256    'getAudioContext' : function getAudioContext() {
    257
    258        // Fallback to Webkit-specific AudioContext implementation
    259        var AudioContext = window.AudioContext || window.webkitAudioContext;
    260
    261        // Get new AudioContext instance if Web Audio API is supported
    262        if (AudioContext) {
    263            try {
    264
    265                // Create new instance if none yet exists
    266                if (!Guacamole.AudioContextFactory.singleton)
    267                    Guacamole.AudioContextFactory.singleton = new AudioContext();
    268
    269                // Return singleton instance
    270                return Guacamole.AudioContextFactory.singleton;
    271
    272            }
    273            catch (e) {
    274                // Do not use Web Audio API if not allowed by browser
    275            }
    276        }
    277
    278        // Web Audio API not supported
    279        return null;
    280
    281    }
    282
    283};
    284/*
    285 * Licensed to the Apache Software Foundation (ASF) under one
    286 * or more contributor license agreements.  See the NOTICE file
    287 * distributed with this work for additional information
    288 * regarding copyright ownership.  The ASF licenses this file
    289 * to you under the Apache License, Version 2.0 (the
    290 * "License"); you may not use this file except in compliance
    291 * with the License.  You may obtain a copy of the License at
    292 *
    293 *   http://www.apache.org/licenses/LICENSE-2.0
    294 *
    295 * Unless required by applicable law or agreed to in writing,
    296 * software distributed under the License is distributed on an
    297 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    298 * KIND, either express or implied.  See the License for the
    299 * specific language governing permissions and limitations
    300 * under the License.
    301 */
    302
    303var Guacamole = Guacamole || {};
    304
    305/**
    306 * Abstract audio player which accepts, queues and plays back arbitrary audio
    307 * data. It is up to implementations of this class to provide some means of
    308 * handling a provided Guacamole.InputStream. Data received along the provided
    309 * stream is to be played back immediately.
    310 *
    311 * @constructor
    312 */
    313Guacamole.AudioPlayer = function AudioPlayer() {
    314
    315    /**
    316     * Notifies this Guacamole.AudioPlayer that all audio up to the current
    317     * point in time has been given via the underlying stream, and that any
    318     * difference in time between queued audio data and the current time can be
    319     * considered latency.
    320     */
    321    this.sync = function sync() {
    322        // Default implementation - do nothing
    323    };
    324
    325};
    326
    327/**
    328 * Determines whether the given mimetype is supported by any built-in
    329 * implementation of Guacamole.AudioPlayer, and thus will be properly handled
    330 * by Guacamole.AudioPlayer.getInstance().
    331 *
    332 * @param {!string} mimetype
    333 *     The mimetype to check.
    334 *
    335 * @returns {!boolean}
    336 *     true if the given mimetype is supported by any built-in
    337 *     Guacamole.AudioPlayer, false otherwise.
    338 */
    339Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) {
    340
    341    return Guacamole.RawAudioPlayer.isSupportedType(mimetype);
    342
    343};
    344
    345/**
    346 * Returns a list of all mimetypes supported by any built-in
    347 * Guacamole.AudioPlayer, in rough order of priority. Beware that only the core
    348 * mimetypes themselves will be listed. Any mimetype parameters, even required
    349 * ones, will not be included in the list. For example, "audio/L8" is a
    350 * supported raw audio mimetype that is supported, but it is invalid without
    351 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
    352 * however (see https://tools.ietf.org/html/rfc4856).
    353 *
    354 * @returns {!string[]}
    355 *     A list of all mimetypes supported by any built-in Guacamole.AudioPlayer,
    356 *     excluding any parameters.
    357 */
    358Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() {
    359
    360    return Guacamole.RawAudioPlayer.getSupportedTypes();
    361
    362};
    363
    364/**
    365 * Returns an instance of Guacamole.AudioPlayer providing support for the given
    366 * audio format. If support for the given audio format is not available, null
    367 * is returned.
    368 *
    369 * @param {!Guacamole.InputStream} stream
    370 *     The Guacamole.InputStream to read audio data from.
    371 *
    372 * @param {!string} mimetype
    373 *     The mimetype of the audio data in the provided stream.
    374 *
    375 * @return {Guacamole.AudioPlayer}
    376 *     A Guacamole.AudioPlayer instance supporting the given mimetype and
    377 *     reading from the given stream, or null if support for the given mimetype
    378 *     is absent.
    379 */
    380Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) {
    381
    382    // Use raw audio player if possible
    383    if (Guacamole.RawAudioPlayer.isSupportedType(mimetype))
    384        return new Guacamole.RawAudioPlayer(stream, mimetype);
    385
    386    // No support for given mimetype
    387    return null;
    388
    389};
    390
    391/**
    392 * Implementation of Guacamole.AudioPlayer providing support for raw PCM format
    393 * audio. This player relies only on the Web Audio API and does not require any
    394 * browser-level support for its audio formats.
    395 *
    396 * @constructor
    397 * @augments Guacamole.AudioPlayer
    398 * @param {!Guacamole.InputStream} stream
    399 *     The Guacamole.InputStream to read audio data from.
    400 *
    401 * @param {!string} mimetype
    402 *     The mimetype of the audio data in the provided stream, which must be a
    403 *     "audio/L8" or "audio/L16" mimetype with necessary parameters, such as:
    404 *     "audio/L16;rate=44100,channels=2".
    405 */
    406Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) {
    407
    408    /**
    409     * The format of audio this player will decode.
    410     *
    411     * @private
    412     * @type {Guacamole.RawAudioFormat}
    413     */
    414    var format = Guacamole.RawAudioFormat.parse(mimetype);
    415
    416    /**
    417     * An instance of a Web Audio API AudioContext object, or null if the
    418     * Web Audio API is not supported.
    419     *
    420     * @private
    421     * @type {AudioContext}
    422     */
    423    var context = Guacamole.AudioContextFactory.getAudioContext();
    424
    425    /**
    426     * The earliest possible time that the next packet could play without
    427     * overlapping an already-playing packet, in seconds. Note that while this
    428     * value is in seconds, it is not an integer value and has microsecond
    429     * resolution.
    430     *
    431     * @private
    432     * @type {!number}
    433     */
    434    var nextPacketTime = context.currentTime;
    435
    436    /**
    437     * Guacamole.ArrayBufferReader wrapped around the audio input stream
    438     * provided with this Guacamole.RawAudioPlayer was created.
    439     *
    440     * @private
    441     * @type {!Guacamole.ArrayBufferReader}
    442     */
    443    var reader = new Guacamole.ArrayBufferReader(stream);
    444
    445    /**
    446     * The minimum size of an audio packet split by splitAudioPacket(), in
    447     * seconds. Audio packets smaller than this will not be split, nor will the
    448     * split result of a larger packet ever be smaller in size than this
    449     * minimum.
    450     *
    451     * @private
    452     * @constant
    453     * @type {!number}
    454     */
    455    var MIN_SPLIT_SIZE = 0.02;
    456
    457    /**
    458     * The maximum amount of latency to allow between the buffered data stream
    459     * and the playback position, in seconds. Initially, this is set to
    460     * roughly one third of a second.
    461     *
    462     * @private
    463     * @type {!number}
    464     */
    465    var maxLatency = 0.3;
    466
    467    /**
    468     * The type of typed array that will be used to represent each audio packet
    469     * internally. This will be either Int8Array or Int16Array, depending on
    470     * whether the raw audio format is 8-bit or 16-bit.
    471     *
    472     * @private
    473     * @constructor
    474     */
    475    var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
    476
    477    /**
    478     * The maximum absolute value of any sample within a raw audio packet
    479     * received by this audio player. This depends only on the size of each
    480     * sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio.
    481     *
    482     * @private
    483     * @type {!number}
    484     */
    485    var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
    486
    487    /**
    488     * The queue of all pending audio packets, as an array of sample arrays.
    489     * Audio packets which are pending playback will be added to this queue for
    490     * further manipulation prior to scheduling via the Web Audio API. Once an
    491     * audio packet leaves this queue and is scheduled via the Web Audio API,
    492     * no further modifications can be made to that packet.
    493     *
    494     * @private
    495     * @type {!SampleArray[]}
    496     */
    497    var packetQueue = [];
    498
    499    /**
    500     * Given an array of audio packets, returns a single audio packet
    501     * containing the concatenation of those packets.
    502     *
    503     * @private
    504     * @param {!SampleArray[]} packets
    505     *     The array of audio packets to concatenate.
    506     *
    507     * @returns {SampleArray}
    508     *     A single audio packet containing the concatenation of all given
    509     *     audio packets. If no packets are provided, this will be undefined.
    510     */
    511    var joinAudioPackets = function joinAudioPackets(packets) {
    512
    513        // Do not bother joining if one or fewer packets are in the queue
    514        if (packets.length <= 1)
    515            return packets[0];
    516
    517        // Determine total sample length of the entire queue
    518        var totalLength = 0;
    519        packets.forEach(function addPacketLengths(packet) {
    520            totalLength += packet.length;
    521        });
    522
    523        // Append each packet within queue
    524        var offset = 0;
    525        var joined = new SampleArray(totalLength);
    526        packets.forEach(function appendPacket(packet) {
    527            joined.set(packet, offset);
    528            offset += packet.length;
    529        });
    530
    531        return joined;
    532
    533    };
    534
    535    /**
    536     * Given a single packet of audio data, splits off an arbitrary length of
    537     * audio data from the beginning of that packet, returning the split result
    538     * as an array of two packets. The split location is determined through an
    539     * algorithm intended to minimize the liklihood of audible clicking between
    540     * packets. If no such split location is possible, an array containing only
    541     * the originally-provided audio packet is returned.
    542     *
    543     * @private
    544     * @param {!SampleArray} data
    545     *     The audio packet to split.
    546     *
    547     * @returns {!SampleArray[]}
    548     *     An array of audio packets containing the result of splitting the
    549     *     provided audio packet. If splitting is possible, this array will
    550     *     contain two packets. If splitting is not possible, this array will
    551     *     contain only the originally-provided packet.
    552     */
    553    var splitAudioPacket = function splitAudioPacket(data) {
    554
    555        var minValue = Number.MAX_VALUE;
    556        var optimalSplitLength = data.length;
    557
    558        // Calculate number of whole samples in the provided audio packet AND
    559        // in the minimum possible split packet
    560        var samples = Math.floor(data.length / format.channels);
    561        var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE);
    562
    563        // Calculate the beginning of the "end" of the audio packet
    564        var start = Math.max(
    565            format.channels * minSplitSamples,
    566            format.channels * (samples - minSplitSamples)
    567        );
    568
    569        // For all samples at the end of the given packet, find a point where
    570        // the perceptible volume across all channels is lowest (and thus is
    571        // the optimal point to split)
    572        for (var offset = start; offset < data.length; offset += format.channels) {
    573
    574            // Calculate the sum of all values across all channels (the result
    575            // will be proportional to the average volume of a sample)
    576            var totalValue = 0;
    577            for (var channel = 0; channel < format.channels; channel++) {
    578                totalValue += Math.abs(data[offset + channel]);
    579            }
    580
    581            // If this is the smallest average value thus far, set the split
    582            // length such that the first packet ends with the current sample
    583            if (totalValue <= minValue) {
    584                optimalSplitLength = offset + format.channels;
    585                minValue = totalValue;
    586            }
    587
    588        }
    589
    590        // If packet is not split, return the supplied packet untouched
    591        if (optimalSplitLength === data.length)
    592            return [data];
    593
    594        // Otherwise, split the packet into two new packets according to the
    595        // calculated optimal split length
    596        return [
    597            new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)),
    598            new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample))
    599        ];
    600
    601    };
    602
    603    /**
    604     * Pushes the given packet of audio data onto the playback queue. Unlike
    605     * other private functions within Guacamole.RawAudioPlayer, the type of the
    606     * ArrayBuffer packet of audio data here need not be specific to the type
    607     * of audio (as with SampleArray). The ArrayBuffer type provided by a
    608     * Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary
    609     * conversions will be performed automatically internally.
    610     *
    611     * @private
    612     * @param {!ArrayBuffer} data
    613     *     A raw packet of audio data that should be pushed onto the audio
    614     *     playback queue.
    615     */
    616    var pushAudioPacket = function pushAudioPacket(data) {
    617        packetQueue.push(new SampleArray(data));
    618    };
    619
    620    /**
    621     * Shifts off and returns a packet of audio data from the beginning of the
    622     * playback queue. The length of this audio packet is determined
    623     * dynamically according to the click-reduction algorithm implemented by
    624     * splitAudioPacket().
    625     *
    626     * @private
    627     * @returns {SampleArray}
    628     *     A packet of audio data pulled from the beginning of the playback
    629     *     queue. If there is no audio currently in the playback queue, this
    630     *     will be null.
    631     */
    632    var shiftAudioPacket = function shiftAudioPacket() {
    633
    634        // Flatten data in packet queue
    635        var data = joinAudioPackets(packetQueue);
    636        if (!data)
    637            return null;
    638
    639        // Pull an appropriate amount of data from the front of the queue
    640        packetQueue = splitAudioPacket(data);
    641        data = packetQueue.shift();
    642
    643        return data;
    644
    645    };
    646
    647    /**
    648     * Converts the given audio packet into an AudioBuffer, ready for playback
    649     * by the Web Audio API. Unlike the raw audio packets received by this
    650     * audio player, AudioBuffers require floating point samples and are split
    651     * into isolated planes of channel-specific data.
    652     *
    653     * @private
    654     * @param {!SampleArray} data
    655     *     The raw audio packet that should be converted into a Web Audio API
    656     *     AudioBuffer.
    657     *
    658     * @returns {!AudioBuffer}
    659     *     A new Web Audio API AudioBuffer containing the provided audio data,
    660     *     converted to the format used by the Web Audio API.
    661     */
    662    var toAudioBuffer = function toAudioBuffer(data) {
    663
    664        // Calculate total number of samples
    665        var samples = data.length / format.channels;
    666
    667        // Determine exactly when packet CAN play
    668        var packetTime = context.currentTime;
    669        if (nextPacketTime < packetTime)
    670            nextPacketTime = packetTime;
    671
    672        // Get audio buffer for specified format
    673        var audioBuffer = context.createBuffer(format.channels, samples, format.rate);
    674
    675        // Convert each channel
    676        for (var channel = 0; channel < format.channels; channel++) {
    677
    678            var audioData = audioBuffer.getChannelData(channel);
    679
    680            // Fill audio buffer with data for channel
    681            var offset = channel;
    682            for (var i = 0; i < samples; i++) {
    683                audioData[i] = data[offset] / maxSampleValue;
    684                offset += format.channels;
    685            }
    686
    687        }
    688
    689        return audioBuffer;
    690
    691    };
    692
    693    // Defer playback of received audio packets slightly
    694    reader.ondata = function playReceivedAudio(data) {
    695
    696        // Push received samples onto queue
    697        pushAudioPacket(new SampleArray(data));
    698
    699        // Shift off an arbitrary packet of audio data from the queue (this may
    700        // be different in size from the packet just pushed)
    701        var packet = shiftAudioPacket();
    702        if (!packet)
    703            return;
    704
    705        // Determine exactly when packet CAN play
    706        var packetTime = context.currentTime;
    707        if (nextPacketTime < packetTime)
    708            nextPacketTime = packetTime;
    709
    710        // Set up buffer source
    711        var source = context.createBufferSource();
    712        source.connect(context.destination);
    713
    714        // Use noteOn() instead of start() if necessary
    715        if (!source.start)
    716            source.start = source.noteOn;
    717
    718        // Schedule packet
    719        source.buffer = toAudioBuffer(packet);
    720        source.start(nextPacketTime);
    721
    722        // Update timeline by duration of scheduled packet
    723        nextPacketTime += packet.length / format.channels / format.rate;
    724
    725    };
    726
    727    /** @override */
    728    this.sync = function sync() {
    729
    730        // Calculate elapsed time since last sync
    731        var now = context.currentTime;
    732
    733        // Reschedule future playback time such that playback latency is
    734        // bounded within a reasonable latency threshold
    735        nextPacketTime = Math.min(nextPacketTime, now + maxLatency);
    736
    737    };
    738
    739};
    740
    741Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer();
    742
    743/**
    744 * Determines whether the given mimetype is supported by
    745 * Guacamole.RawAudioPlayer.
    746 *
    747 * @param {!string} mimetype
    748 *     The mimetype to check.
    749 *
    750 * @returns {!boolean}
    751 *     true if the given mimetype is supported by Guacamole.RawAudioPlayer,
    752 *     false otherwise.
    753 */
    754Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) {
    755
    756    // No supported types if no Web Audio API
    757    if (!Guacamole.AudioContextFactory.getAudioContext())
    758        return false;
    759
    760    return Guacamole.RawAudioFormat.parse(mimetype) !== null;
    761
    762};
    763
    764/**
    765 * Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only
    766 * the core mimetypes themselves will be listed. Any mimetype parameters, even
    767 * required ones, will not be included in the list. For example, "audio/L8" is
    768 * a raw audio mimetype that may be supported, but it is invalid without
    769 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
    770 * however (see https://tools.ietf.org/html/rfc4856).
    771 *
    772 * @returns {!string[]}
    773 *     A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding
    774 *     any parameters. If the necessary JavaScript APIs for playing raw audio
    775 *     are absent, this list will be empty.
    776 */
    777Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() {
    778
    779    // No supported types if no Web Audio API
    780    if (!Guacamole.AudioContextFactory.getAudioContext())
    781        return [];
    782
    783    // We support 8-bit and 16-bit raw PCM
    784    return [
    785        'audio/L8',
    786        'audio/L16'
    787    ];
    788
    789};
    790/*
    791 * Licensed to the Apache Software Foundation (ASF) under one
    792 * or more contributor license agreements.  See the NOTICE file
    793 * distributed with this work for additional information
    794 * regarding copyright ownership.  The ASF licenses this file
    795 * to you under the Apache License, Version 2.0 (the
    796 * "License"); you may not use this file except in compliance
    797 * with the License.  You may obtain a copy of the License at
    798 *
    799 *   http://www.apache.org/licenses/LICENSE-2.0
    800 *
    801 * Unless required by applicable law or agreed to in writing,
    802 * software distributed under the License is distributed on an
    803 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    804 * KIND, either express or implied.  See the License for the
    805 * specific language governing permissions and limitations
    806 * under the License.
    807 */
    808
    809var Guacamole = Guacamole || {};
    810
    811/**
    812 * Abstract audio recorder which streams arbitrary audio data to an underlying
    813 * Guacamole.OutputStream. It is up to implementations of this class to provide
    814 * some means of handling this Guacamole.OutputStream. Data produced by the
    815 * recorder is to be sent along the provided stream immediately.
    816 *
    817 * @constructor
    818 */
    819Guacamole.AudioRecorder = function AudioRecorder() {
    820
    821    /**
    822     * Callback which is invoked when the audio recording process has stopped
    823     * and the underlying Guacamole stream has been closed normally. Audio will
    824     * only resume recording if a new Guacamole.AudioRecorder is started. This
    825     * Guacamole.AudioRecorder instance MAY NOT be reused.
    826     *
    827     * @event
    828     */
    829    this.onclose = null;
    830
    831    /**
    832     * Callback which is invoked when the audio recording process cannot
    833     * continue due to an error, if it has started at all. The underlying
    834     * Guacamole stream is automatically closed. Future attempts to record
    835     * audio should not be made, and this Guacamole.AudioRecorder instance
    836     * MAY NOT be reused.
    837     *
    838     * @event
    839     */
    840    this.onerror = null;
    841
    842};
    843
    844/**
    845 * Determines whether the given mimetype is supported by any built-in
    846 * implementation of Guacamole.AudioRecorder, and thus will be properly handled
    847 * by Guacamole.AudioRecorder.getInstance().
    848 *
    849 * @param {!string} mimetype
    850 *     The mimetype to check.
    851 *
    852 * @returns {!boolean}
    853 *     true if the given mimetype is supported by any built-in
    854 *     Guacamole.AudioRecorder, false otherwise.
    855 */
    856Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {
    857
    858    return Guacamole.RawAudioRecorder.isSupportedType(mimetype);
    859
    860};
    861
    862/**
    863 * Returns a list of all mimetypes supported by any built-in
    864 * Guacamole.AudioRecorder, in rough order of priority. Beware that only the
    865 * core mimetypes themselves will be listed. Any mimetype parameters, even
    866 * required ones, will not be included in the list. For example, "audio/L8" is
    867 * a supported raw audio mimetype that is supported, but it is invalid without
    868 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
    869 * however (see https://tools.ietf.org/html/rfc4856).
    870 *
    871 * @returns {!string[]}
    872 *     A list of all mimetypes supported by any built-in
    873 *     Guacamole.AudioRecorder, excluding any parameters.
    874 */
    875Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {
    876
    877    return Guacamole.RawAudioRecorder.getSupportedTypes();
    878
    879};
    880
    881/**
    882 * Returns an instance of Guacamole.AudioRecorder providing support for the
    883 * given audio format. If support for the given audio format is not available,
    884 * null is returned.
    885 *
    886 * @param {!Guacamole.OutputStream} stream
    887 *     The Guacamole.OutputStream to send audio data through.
    888 *
    889 * @param {!string} mimetype
    890 *     The mimetype of the audio data to be sent along the provided stream.
    891 *
    892 * @return {Guacamole.AudioRecorder}
    893 *     A Guacamole.AudioRecorder instance supporting the given mimetype and
    894 *     writing to the given stream, or null if support for the given mimetype
    895 *     is absent.
    896 */
    897Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {
    898
    899    // Use raw audio recorder if possible
    900    if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
    901        return new Guacamole.RawAudioRecorder(stream, mimetype);
    902
    903    // No support for given mimetype
    904    return null;
    905
    906};
    907
    908/**
    909 * Implementation of Guacamole.AudioRecorder providing support for raw PCM
    910 * format audio. This recorder relies only on the Web Audio API and does not
    911 * require any browser-level support for its audio formats.
    912 *
    913 * @constructor
    914 * @augments Guacamole.AudioRecorder
    915 * @param {!Guacamole.OutputStream} stream
    916 *     The Guacamole.OutputStream to write audio data to.
    917 *
    918 * @param {!string} mimetype
    919 *     The mimetype of the audio data to send along the provided stream, which
    920 *     must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
    921 *     such as: "audio/L16;rate=44100,channels=2".
    922 */
    923Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) {
    924
    925    /**
    926     * Reference to this RawAudioRecorder.
    927     *
    928     * @private
    929     * @type {!Guacamole.RawAudioRecorder}
    930     */
    931    var recorder = this;
    932
    933    /**
    934     * The size of audio buffer to request from the Web Audio API when
    935     * recording or processing audio, in sample-frames. This must be a power of
    936     * two between 256 and 16384 inclusive, as required by
    937     * AudioContext.createScriptProcessor().
    938     *
    939     * @private
    940     * @constant
    941     * @type {!number}
    942     */
    943    var BUFFER_SIZE = 2048;
    944
    945    /**
    946     * The window size to use when applying Lanczos interpolation, commonly
    947     * denoted by the variable "a".
    948     * See: https://en.wikipedia.org/wiki/Lanczos_resampling
    949     *
    950     * @private
    951     * @contant
    952     * @type {!number}
    953     */
    954    var LANCZOS_WINDOW_SIZE = 3;
    955
    956    /**
    957     * The format of audio this recorder will encode.
    958     *
    959     * @private
    960     * @type {Guacamole.RawAudioFormat}
    961     */
    962    var format = Guacamole.RawAudioFormat.parse(mimetype);
    963
    964    /**
    965     * An instance of a Web Audio API AudioContext object, or null if the
    966     * Web Audio API is not supported.
    967     *
    968     * @private
    969     * @type {AudioContext}
    970     */
    971    var context = Guacamole.AudioContextFactory.getAudioContext();
    972
    973    // Some browsers do not implement navigator.mediaDevices - this
    974    // shims in this functionality to ensure code compatibility.
    975    if (!navigator.mediaDevices)
    976        navigator.mediaDevices = {};
    977
    978    // Browsers that either do not implement navigator.mediaDevices
    979    // at all or do not implement it completely need the getUserMedia
    980    // method defined.  This shims in this function by detecting
    981    // one of the supported legacy methods.
    982    if (!navigator.mediaDevices.getUserMedia)
    983        navigator.mediaDevices.getUserMedia = (navigator.getUserMedia
    984                || navigator.webkitGetUserMedia
    985                || navigator.mozGetUserMedia
    986                || navigator.msGetUserMedia).bind(navigator);
    987
    988    /**
    989     * Guacamole.ArrayBufferWriter wrapped around the audio output stream
    990     * provided when this Guacamole.RawAudioRecorder was created.
    991     *
    992     * @private
    993     * @type {!Guacamole.ArrayBufferWriter}
    994     */
    995    var writer = new Guacamole.ArrayBufferWriter(stream);
    996
    997    /**
    998     * The type of typed array that will be used to represent each audio packet
    999     * internally. This will be either Int8Array or Int16Array, depending on
   1000     * whether the raw audio format is 8-bit or 16-bit.
   1001     *
   1002     * @private
   1003     * @constructor
   1004     */
   1005    var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
   1006
   1007    /**
   1008     * The maximum absolute value of any sample within a raw audio packet sent
   1009     * by this audio recorder. This depends only on the size of each sample,
   1010     * and will be 128 for 8-bit audio and 32768 for 16-bit audio.
   1011     *
   1012     * @private
   1013     * @type {!number}
   1014     */
   1015    var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
   1016
   1017    /**
   1018     * The total number of audio samples read from the local audio input device
   1019     * over the life of this audio recorder.
   1020     *
   1021     * @private
   1022     * @type {!number}
   1023     */
   1024    var readSamples = 0;
   1025
   1026    /**
   1027     * The total number of audio samples written to the underlying Guacamole
   1028     * connection over the life of this audio recorder.
   1029     *
   1030     * @private
   1031     * @type {!number}
   1032     */
   1033    var writtenSamples = 0;
   1034
   1035    /**
   1036     * The audio stream provided by the browser, if allowed. If no stream has
   1037     * yet been received, this will be null.
   1038     *
   1039     * @private
   1040     * @type {MediaStream}
   1041     */
   1042    var mediaStream = null;
   1043
   1044    /**
   1045     * The source node providing access to the local audio input device.
   1046     *
   1047     * @private
   1048     * @type {MediaStreamAudioSourceNode}
   1049     */
   1050    var source = null;
   1051
   1052    /**
   1053     * The script processing node which receives audio input from the media
   1054     * stream source node as individual audio buffers.
   1055     *
   1056     * @private
   1057     * @type {ScriptProcessorNode}
   1058     */
   1059    var processor = null;
   1060
   1061    /**
   1062     * The normalized sinc function. The normalized sinc function is defined as
   1063     * 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
   1064     *
   1065     * See: https://en.wikipedia.org/wiki/Sinc_function
   1066     *
   1067     * @private
   1068     * @param {!number} x
   1069     *     The point at which the normalized sinc function should be computed.
   1070     *
   1071     * @returns {!number}
   1072     *     The value of the normalized sinc function at x.
   1073     */
   1074    var sinc = function sinc(x) {
   1075
   1076        // The value of sinc(0) is defined as 1
   1077        if (x === 0)
   1078            return 1;
   1079
   1080        // Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
   1081        var piX = Math.PI * x;
   1082        return Math.sin(piX) / piX;
   1083
   1084    };
   1085
   1086    /**
   1087     * Calculates the value of the Lanczos kernal at point x for a given window
   1088     * size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
   1089     *
   1090     * @private
   1091     * @param {!number} x
   1092     *     The point at which the value of the Lanczos kernel should be
   1093     *     computed.
   1094     *
   1095     * @param {!number} a
   1096     *     The window size to use for the Lanczos kernel.
   1097     *
   1098     * @returns {!number}
   1099     *     The value of the Lanczos kernel at the given point for the given
   1100     *     window size.
   1101     */
   1102    var lanczos = function lanczos(x, a) {
   1103
   1104        // Lanczos is sinc(x) * sinc(x / a) for -a < x < a ...
   1105        if (-a < x && x < a)
   1106            return sinc(x) * sinc(x / a);
   1107
   1108        // ... and 0 otherwise
   1109        return 0;
   1110
   1111    };
   1112
   1113    /**
   1114     * Determines the value of the waveform represented by the audio data at
   1115     * the given location. If the value cannot be determined exactly as it does
   1116     * not correspond to an exact sample within the audio data, the value will
   1117     * be derived through interpolating nearby samples.
   1118     *
   1119     * @private
   1120     * @param {!Float32Array} audioData
   1121     *     An array of audio data, as returned by AudioBuffer.getChannelData().
   1122     *
   1123     * @param {!number} t
   1124     *     The relative location within the waveform from which the value
   1125     *     should be retrieved, represented as a floating point number between
   1126     *     0 and 1 inclusive, where 0 represents the earliest point in time and
   1127     *     1 represents the latest.
   1128     *
   1129     * @returns {!number}
   1130     *     The value of the waveform at the given location.
   1131     */
   1132    var interpolateSample = function getValueAt(audioData, t) {
   1133
   1134        // Convert [0, 1] range to [0, audioData.length - 1]
   1135        var index = (audioData.length - 1) * t;
   1136
   1137        // Determine the start and end points for the summation used by the
   1138        // Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling)
   1139        var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1;
   1140        var end = Math.floor(index) + LANCZOS_WINDOW_SIZE;
   1141
   1142        // Calculate the value of the Lanczos interpolation function for the
   1143        // required range
   1144        var sum = 0;
   1145        for (var i = start; i <= end; i++) {
   1146            sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE);
   1147        }
   1148
   1149        return sum;
   1150
   1151    };
   1152
   1153    /**
   1154     * Converts the given AudioBuffer into an audio packet, ready for streaming
   1155     * along the underlying output stream. Unlike the raw audio packets used by
   1156     * this audio recorder, AudioBuffers require floating point samples and are
   1157     * split into isolated planes of channel-specific data.
   1158     *
   1159     * @private
   1160     * @param {!AudioBuffer} audioBuffer
   1161     *     The Web Audio API AudioBuffer that should be converted to a raw
   1162     *     audio packet.
   1163     *
   1164     * @returns {!SampleArray}
   1165     *     A new raw audio packet containing the audio data from the provided
   1166     *     AudioBuffer.
   1167     */
   1168    var toSampleArray = function toSampleArray(audioBuffer) {
   1169
   1170        // Track overall amount of data read
   1171        var inSamples = audioBuffer.length;
   1172        readSamples += inSamples;
   1173
   1174        // Calculate the total number of samples that should be written as of
   1175        // the audio data just received and adjust the size of the output
   1176        // packet accordingly
   1177        var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate);
   1178        var outSamples = expectedWrittenSamples - writtenSamples;
   1179
   1180        // Update number of samples written
   1181        writtenSamples += outSamples;
   1182
   1183        // Get array for raw PCM storage
   1184        var data = new SampleArray(outSamples * format.channels);
   1185
   1186        // Convert each channel
   1187        for (var channel = 0; channel < format.channels; channel++) {
   1188
   1189            var audioData = audioBuffer.getChannelData(channel);
   1190
   1191            // Fill array with data from audio buffer channel
   1192            var offset = channel;
   1193            for (var i = 0; i < outSamples; i++) {
   1194                data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue;
   1195                offset += format.channels;
   1196            }
   1197
   1198        }
   1199
   1200        return data;
   1201
   1202    };
   1203
   1204    /**
   1205     * getUserMedia() callback which handles successful retrieval of an
   1206     * audio stream (successful start of recording).
   1207     *
   1208     * @private
   1209     * @param {!MediaStream} stream
   1210     *     A MediaStream which provides access to audio data read from the
   1211     *     user's local audio input device.
   1212     */
   1213    var streamReceived = function streamReceived(stream) {
   1214
   1215        // Create processing node which receives appropriately-sized audio buffers
   1216        processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels);
   1217        processor.connect(context.destination);
   1218
   1219        // Send blobs when audio buffers are received
   1220        processor.onaudioprocess = function processAudio(e) {
   1221            writer.sendData(toSampleArray(e.inputBuffer).buffer);
   1222        };
   1223
   1224        // Connect processing node to user's audio input source
   1225        source = context.createMediaStreamSource(stream);
   1226        source.connect(processor);
   1227
   1228        // Attempt to explicitly resume AudioContext, as it may be paused
   1229        // by default
   1230        if (context.state === 'suspended')
   1231            context.resume();
   1232
   1233        // Save stream for later cleanup
   1234        mediaStream = stream;
   1235
   1236    };
   1237
   1238    /**
   1239     * getUserMedia() callback which handles audio recording denial. The
   1240     * underlying Guacamole output stream is closed, and the failure to
   1241     * record is noted using onerror.
   1242     *
   1243     * @private
   1244     */
   1245    var streamDenied = function streamDenied() {
   1246
   1247        // Simply end stream if audio access is not allowed
   1248        writer.sendEnd();
   1249
   1250        // Notify of closure
   1251        if (recorder.onerror)
   1252            recorder.onerror();
   1253
   1254    };
   1255
   1256    /**
   1257     * Requests access to the user's microphone and begins capturing audio. All
   1258     * received audio data is resampled as necessary and forwarded to the
   1259     * Guacamole stream underlying this Guacamole.RawAudioRecorder. This
   1260     * function must be invoked ONLY ONCE per instance of
   1261     * Guacamole.RawAudioRecorder.
   1262     *
   1263     * @private
   1264     */
   1265    var beginAudioCapture = function beginAudioCapture() {
   1266
   1267        // Attempt to retrieve an audio input stream from the browser
   1268        var promise = navigator.mediaDevices.getUserMedia({
   1269            'audio' : true
   1270        }, streamReceived, streamDenied);
   1271
   1272        // Handle stream creation/rejection via Promise for newer versions of
   1273        // getUserMedia()
   1274        if (promise && promise.then)
   1275            promise.then(streamReceived, streamDenied);
   1276
   1277    };
   1278
   1279    /**
   1280     * Stops capturing audio, if the capture has started, freeing all associated
   1281     * resources. If the capture has not started, this function simply ends the
   1282     * underlying Guacamole stream.
   1283     *
   1284     * @private
   1285     */
   1286    var stopAudioCapture = function stopAudioCapture() {
   1287
   1288        // Disconnect media source node from script processor
   1289        if (source)
   1290            source.disconnect();
   1291
   1292        // Disconnect associated script processor node
   1293        if (processor)
   1294            processor.disconnect();
   1295
   1296        // Stop capture
   1297        if (mediaStream) {
   1298            var tracks = mediaStream.getTracks();
   1299            for (var i = 0; i < tracks.length; i++)
   1300                tracks[i].stop();
   1301        }
   1302
   1303        // Remove references to now-unneeded components
   1304        processor = null;
   1305        source = null;
   1306        mediaStream = null;
   1307
   1308        // End stream
   1309        writer.sendEnd();
   1310
   1311    };
   1312
   1313    // Once audio stream is successfully open, request and begin reading audio
   1314    writer.onack = function audioStreamAcknowledged(status) {
   1315
   1316        // Begin capture if successful response and not yet started
   1317        if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream)
   1318            beginAudioCapture();
   1319
   1320        // Otherwise stop capture and cease handling any further acks
   1321        else {
   1322
   1323            // Stop capturing audio
   1324            stopAudioCapture();
   1325            writer.onack = null;
   1326
   1327            // Notify if stream has closed normally
   1328            if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) {
   1329                if (recorder.onclose)
   1330                    recorder.onclose();
   1331            }
   1332
   1333            // Otherwise notify of closure due to error
   1334            else {
   1335                if (recorder.onerror)
   1336                    recorder.onerror();
   1337            }
   1338
   1339        }
   1340
   1341    };
   1342
   1343};
   1344
   1345Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder();
   1346
   1347/**
   1348 * Determines whether the given mimetype is supported by
   1349 * Guacamole.RawAudioRecorder.
   1350 *
   1351 * @param {!string} mimetype
   1352 *     The mimetype to check.
   1353 *
   1354 * @returns {!boolean}
   1355 *     true if the given mimetype is supported by Guacamole.RawAudioRecorder,
   1356 *     false otherwise.
   1357 */
   1358Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) {
   1359
   1360    // No supported types if no Web Audio API
   1361    if (!Guacamole.AudioContextFactory.getAudioContext())
   1362        return false;
   1363
   1364    return Guacamole.RawAudioFormat.parse(mimetype) !== null;
   1365
   1366};
   1367
   1368/**
   1369 * Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only
   1370 * the core mimetypes themselves will be listed. Any mimetype parameters, even
   1371 * required ones, will not be included in the list. For example, "audio/L8" is
   1372 * a raw audio mimetype that may be supported, but it is invalid without
   1373 * additional parameters. Something like "audio/L8;rate=44100" would be valid,
   1374 * however (see https://tools.ietf.org/html/rfc4856).
   1375 *
   1376 * @returns {!string[]}
   1377 *     A list of all mimetypes supported by Guacamole.RawAudioRecorder,
   1378 *     excluding any parameters. If the necessary JavaScript APIs for recording
   1379 *     raw audio are absent, this list will be empty.
   1380 */
   1381Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() {
   1382
   1383    // No supported types if no Web Audio API
   1384    if (!Guacamole.AudioContextFactory.getAudioContext())
   1385        return [];
   1386
   1387    // We support 8-bit and 16-bit raw PCM
   1388    return [
   1389        'audio/L8',
   1390        'audio/L16'
   1391    ];
   1392
   1393};
   1394/*
   1395 * Licensed to the Apache Software Foundation (ASF) under one
   1396 * or more contributor license agreements.  See the NOTICE file
   1397 * distributed with this work for additional information
   1398 * regarding copyright ownership.  The ASF licenses this file
   1399 * to you under the Apache License, Version 2.0 (the
   1400 * "License"); you may not use this file except in compliance
   1401 * with the License.  You may obtain a copy of the License at
   1402 *
   1403 *   http://www.apache.org/licenses/LICENSE-2.0
   1404 *
   1405 * Unless required by applicable law or agreed to in writing,
   1406 * software distributed under the License is distributed on an
   1407 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   1408 * KIND, either express or implied.  See the License for the
   1409 * specific language governing permissions and limitations
   1410 * under the License.
   1411 */
   1412
   1413var Guacamole = Guacamole || {};
   1414
   1415/**
   1416 * A reader which automatically handles the given input stream, assembling all
   1417 * received blobs into a single blob by appending them to each other in order.
   1418 * Note that this object will overwrite any installed event handlers on the
   1419 * given Guacamole.InputStream.
   1420 * 
   1421 * @constructor
   1422 * @param {!Guacamole.InputStream} stream
   1423 *     The stream that data will be read from.
   1424 *
   1425 * @param {!string} mimetype
   1426 *     The mimetype of the blob being built.
   1427 */
   1428Guacamole.BlobReader = function(stream, mimetype) {
   1429
   1430    /**
   1431     * Reference to this Guacamole.InputStream.
   1432     *
   1433     * @private
   1434     * @type {!Guacamole.BlobReader}
   1435     */
   1436    var guac_reader = this;
   1437
   1438    /**
   1439     * The length of this Guacamole.InputStream in bytes.
   1440     *
   1441     * @private
   1442     * @type {!number}
   1443     */
   1444    var length = 0;
   1445
   1446    // Get blob builder
   1447    var blob_builder;
   1448    if      (window.BlobBuilder)       blob_builder = new BlobBuilder();
   1449    else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder();
   1450    else if (window.MozBlobBuilder)    blob_builder = new MozBlobBuilder();
   1451    else
   1452        blob_builder = new (function() {
   1453
   1454            var blobs = [];
   1455
   1456            /** @ignore */
   1457            this.append = function(data) {
   1458                blobs.push(new Blob([data], {"type": mimetype}));
   1459            };
   1460
   1461            /** @ignore */
   1462            this.getBlob = function() {
   1463                return new Blob(blobs, {"type": mimetype});
   1464            };
   1465
   1466        })();
   1467
   1468    // Append received blobs
   1469    stream.onblob = function(data) {
   1470
   1471        // Convert to ArrayBuffer
   1472        var binary = window.atob(data);
   1473        var arrayBuffer = new ArrayBuffer(binary.length);
   1474        var bufferView = new Uint8Array(arrayBuffer);
   1475
   1476        for (var i=0; i<binary.length; i++)
   1477            bufferView[i] = binary.charCodeAt(i);
   1478
   1479        blob_builder.append(arrayBuffer);
   1480        length += arrayBuffer.byteLength;
   1481
   1482        // Call handler, if present
   1483        if (guac_reader.onprogress)
   1484            guac_reader.onprogress(arrayBuffer.byteLength);
   1485
   1486        // Send success response
   1487        stream.sendAck("OK", 0x0000);
   1488
   1489    };
   1490
   1491    // Simply call onend when end received
   1492    stream.onend = function() {
   1493        if (guac_reader.onend)
   1494            guac_reader.onend();
   1495    };
   1496
   1497    /**
   1498     * Returns the current length of this Guacamole.InputStream, in bytes.
   1499     *
   1500     * @return {!number}
   1501     *     The current length of this Guacamole.InputStream.
   1502     */
   1503    this.getLength = function() {
   1504        return length;
   1505    };
   1506
   1507    /**
   1508     * Returns the contents of this Guacamole.BlobReader as a Blob.
   1509     *
   1510     * @return {!Blob}
   1511     *     The contents of this Guacamole.BlobReader.
   1512     */
   1513    this.getBlob = function() {
   1514        return blob_builder.getBlob();
   1515    };
   1516
   1517    /**
   1518     * Fired once for every blob of data received.
   1519     * 
   1520     * @event
   1521     * @param {!number} length
   1522     *     The number of bytes received.
   1523     */
   1524    this.onprogress = null;
   1525
   1526    /**
   1527     * Fired once this stream is finished and no further data will be written.
   1528     * @event
   1529     */
   1530    this.onend = null;
   1531
   1532};/*
   1533 * Licensed to the Apache Software Foundation (ASF) under one
   1534 * or more contributor license agreements.  See the NOTICE file
   1535 * distributed with this work for additional information
   1536 * regarding copyright ownership.  The ASF licenses this file
   1537 * to you under the Apache License, Version 2.0 (the
   1538 * "License"); you may not use this file except in compliance
   1539 * with the License.  You may obtain a copy of the License at
   1540 *
   1541 *   http://www.apache.org/licenses/LICENSE-2.0
   1542 *
   1543 * Unless required by applicable law or agreed to in writing,
   1544 * software distributed under the License is distributed on an
   1545 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   1546 * KIND, either express or implied.  See the License for the
   1547 * specific language governing permissions and limitations
   1548 * under the License.
   1549 */
   1550
   1551var Guacamole = Guacamole || {};
   1552
   1553/**
   1554 * A writer which automatically writes to the given output stream with the
   1555 * contents of provided Blob objects.
   1556 *
   1557 * @constructor
   1558 * @param {!Guacamole.OutputStream} stream
   1559 *     The stream that data will be written to.
   1560 */
   1561Guacamole.BlobWriter = function BlobWriter(stream) {
   1562
   1563    /**
   1564     * Reference to this Guacamole.BlobWriter.
   1565     *
   1566     * @private
   1567     * @type {!Guacamole.BlobWriter}
   1568     */
   1569    var guacWriter = this;
   1570
   1571    /**
   1572     * Wrapped Guacamole.ArrayBufferWriter which will be used to send any
   1573     * provided file data.
   1574     *
   1575     * @private
   1576     * @type {!Guacamole.ArrayBufferWriter}
   1577     */
   1578    var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream);
   1579
   1580    // Initially, simply call onack for acknowledgements
   1581    arrayBufferWriter.onack = function(status) {
   1582        if (guacWriter.onack)
   1583            guacWriter.onack(status);
   1584    };
   1585
   1586    /**
   1587     * Browser-independent implementation of Blob.slice() which uses an end
   1588     * offset to determine the span of the resulting slice, rather than a
   1589     * length.
   1590     *
   1591     * @private
   1592     * @param {!Blob} blob
   1593     *     The Blob to slice.
   1594     *
   1595     * @param {!number} start
   1596     *     The starting offset of the slice, in bytes, inclusive.
   1597     *
   1598     * @param {!number} end
   1599     *     The ending offset of the slice, in bytes, exclusive.
   1600     *
   1601     * @returns {!Blob}
   1602     *     A Blob containing the data within the given Blob starting at
   1603     *     <code>start</code> and ending at <code>end - 1</code>.
   1604     */
   1605    var slice = function slice(blob, start, end) {
   1606
   1607        // Use prefixed implementations if necessary
   1608        var sliceImplementation = (
   1609                blob.slice
   1610             || blob.webkitSlice
   1611             || blob.mozSlice
   1612        ).bind(blob);
   1613
   1614        var length = end - start;
   1615
   1616        // The old Blob.slice() was length-based (not end-based). Try the
   1617        // length version first, if the two calls are not equivalent.
   1618        if (length !== end) {
   1619
   1620            // If the result of the slice() call matches the expected length,
   1621            // trust that result. It must be correct.
   1622            var sliceResult = sliceImplementation(start, length);
   1623            if (sliceResult.size === length)
   1624                return sliceResult;
   1625
   1626        }
   1627
   1628        // Otherwise, use the most-recent standard: end-based slice()
   1629        return sliceImplementation(start, end);
   1630
   1631    };
   1632
   1633    /**
   1634     * Sends the contents of the given blob over the underlying stream.
   1635     *
   1636     * @param {!Blob} blob
   1637     *     The blob to send.
   1638     */
   1639    this.sendBlob = function sendBlob(blob) {
   1640
   1641        var offset = 0;
   1642        var reader = new FileReader();
   1643
   1644        /**
   1645         * Reads the next chunk of the blob provided to
   1646         * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The chunk itself
   1647         * is read asynchronously, and will not be available until
   1648         * reader.onload fires.
   1649         *
   1650         * @private
   1651         */
   1652        var readNextChunk = function readNextChunk() {
   1653
   1654            // If no further chunks remain, inform of completion and stop
   1655            if (offset >= blob.size) {
   1656
   1657                // Fire completion event for completed blob
   1658                if (guacWriter.oncomplete)
   1659                    guacWriter.oncomplete(blob);
   1660
   1661                // No further chunks to read
   1662                return;
   1663
   1664            }
   1665
   1666            // Obtain reference to next chunk as a new blob
   1667            var chunk = slice(blob, offset, offset + arrayBufferWriter.blobLength);
   1668            offset += arrayBufferWriter.blobLength;
   1669
   1670            // Attempt to read the blob contents represented by the blob into
   1671            // a new array buffer
   1672            reader.readAsArrayBuffer(chunk);
   1673
   1674        };
   1675
   1676        // Send each chunk over the stream, continue reading the next chunk
   1677        reader.onload = function chunkLoadComplete() {
   1678
   1679            // Send the successfully-read chunk
   1680            arrayBufferWriter.sendData(reader.result);
   1681
   1682            // Continue sending more chunks after the latest chunk is
   1683            // acknowledged
   1684            arrayBufferWriter.onack = function sendMoreChunks(status) {
   1685
   1686                if (guacWriter.onack)
   1687                    guacWriter.onack(status);
   1688
   1689                // Abort transfer if an error occurs
   1690                if (status.isError())
   1691                    return;
   1692
   1693                // Inform of blob upload progress via progress events
   1694                if (guacWriter.onprogress)
   1695                    guacWriter.onprogress(blob, offset - arrayBufferWriter.blobLength);
   1696
   1697                // Queue the next chunk for reading
   1698                readNextChunk();
   1699
   1700            };
   1701
   1702        };
   1703
   1704        // If an error prevents further reading, inform of error and stop
   1705        reader.onerror = function chunkLoadFailed() {
   1706
   1707            // Fire error event, including the context of the error
   1708            if (guacWriter.onerror)
   1709                guacWriter.onerror(blob, offset, reader.error);
   1710
   1711        };
   1712
   1713        // Begin reading the first chunk
   1714        readNextChunk();
   1715
   1716    };
   1717
   1718    /**
   1719     * Signals that no further text will be sent, effectively closing the
   1720     * stream.
   1721     */
   1722    this.sendEnd = function sendEnd() {
   1723        arrayBufferWriter.sendEnd();
   1724    };
   1725
   1726    /**
   1727     * Fired for received data, if acknowledged by the server.
   1728     *
   1729     * @event
   1730     * @param {!Guacamole.Status} status
   1731     *     The status of the operation.
   1732     */
   1733    this.onack = null;
   1734
   1735    /**
   1736     * Fired when an error occurs reading a blob passed to
   1737     * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The transfer for the
   1738     * the given blob will cease, but the stream will remain open.
   1739     *
   1740     * @event
   1741     * @param {!Blob} blob
   1742     *     The blob that was being read when the error occurred.
   1743     *
   1744     * @param {!number} offset
   1745     *     The offset of the failed read attempt within the blob, in bytes.
   1746     *
   1747     * @param {!DOMError} error
   1748     *     The error that occurred.
   1749     */
   1750    this.onerror = null;
   1751
   1752    /**
   1753     * Fired for each successfully-read chunk of data as a blob is being sent
   1754     * via [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}.
   1755     *
   1756     * @event
   1757     * @param {!Blob} blob
   1758     *     The blob that is being read.
   1759     *
   1760     * @param {!number} offset
   1761     *     The offset of the read that just succeeded.
   1762     */
   1763    this.onprogress = null;
   1764
   1765    /**
   1766     * Fired when a blob passed to
   1767     * [sendBlob()]{@link Guacamole.BlobWriter#sendBlob} has finished being
   1768     * sent.
   1769     *
   1770     * @event
   1771     * @param {!Blob} blob
   1772     *     The blob that was sent.
   1773     */
   1774    this.oncomplete = null;
   1775
   1776};
   1777/*
   1778 * Licensed to the Apache Software Foundation (ASF) under one
   1779 * or more contributor license agreements.  See the NOTICE file
   1780 * distributed with this work for additional information
   1781 * regarding copyright ownership.  The ASF licenses this file
   1782 * to you under the Apache License, Version 2.0 (the
   1783 * "License"); you may not use this file except in compliance
   1784 * with the License.  You may obtain a copy of the License at
   1785 *
   1786 *   http://www.apache.org/licenses/LICENSE-2.0
   1787 *
   1788 * Unless required by applicable law or agreed to in writing,
   1789 * software distributed under the License is distributed on an
   1790 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   1791 * KIND, either express or implied.  See the License for the
   1792 * specific language governing permissions and limitations
   1793 * under the License.
   1794 */
   1795
   1796var Guacamole = Guacamole || {};
   1797
   1798/**
   1799 * Guacamole protocol client. Given a {@link Guacamole.Tunnel},
   1800 * automatically handles incoming and outgoing Guacamole instructions via the
   1801 * provided tunnel, updating its display using one or more canvas elements.
   1802 * 
   1803 * @constructor
   1804 * @param {!Guacamole.Tunnel} tunnel
   1805 *     The tunnel to use to send and receive Guacamole instructions.
   1806 */
   1807Guacamole.Client = function(tunnel) {
   1808
   1809    var guac_client = this;
   1810
   1811    var currentState = Guacamole.Client.State.IDLE;
   1812    
   1813    var currentTimestamp = 0;
   1814
   1815    /**
   1816     * The rough number of milliseconds to wait between sending keep-alive
   1817     * pings. This may vary depending on how frequently the browser allows
   1818     * timers to run, as well as how frequently the client receives messages
   1819     * from the server.
   1820     *
   1821     * @private
   1822     * @constant
   1823     * @type {!number}
   1824     */
   1825    var KEEP_ALIVE_FREQUENCY = 5000;
   1826
   1827    /**
   1828     * The current keep-alive ping timeout ID, if any. This will only be set
   1829     * upon connecting.
   1830     *
   1831     * @private
   1832     * @type {number}
   1833     */
   1834    var keepAliveTimeout = null;
   1835
   1836    /**
   1837     * The timestamp of the point in time that the last keep-live ping was
   1838     * sent, in milliseconds elapsed since midnight of January 1, 1970 UTC.
   1839     *
   1840     * @private
   1841     * @type {!number}
   1842     */
   1843    var lastSentKeepAlive = 0;
   1844
   1845    /**
   1846     * Translation from Guacamole protocol line caps to Layer line caps.
   1847     *
   1848     * @private
   1849     * @type {!Object.<number, string>}
   1850     */
   1851    var lineCap = {
   1852        0: "butt",
   1853        1: "round",
   1854        2: "square"
   1855    };
   1856
   1857    /**
   1858     * Translation from Guacamole protocol line caps to Layer line caps.
   1859     *
   1860     * @private
   1861     * @type {!Object.<number, string>}
   1862     */
   1863    var lineJoin = {
   1864        0: "bevel",
   1865        1: "miter",
   1866        2: "round"
   1867    };
   1868
   1869    /**
   1870     * The underlying Guacamole display.
   1871     *
   1872     * @private
   1873     * @type {!Guacamole.Display}
   1874     */
   1875    var display = new Guacamole.Display();
   1876
   1877    /**
   1878     * All available layers and buffers
   1879     *
   1880     * @private
   1881     * @type {!Object.<number, (Guacamole.Display.VisibleLayer|Guacamole.Layer)>}
   1882     */
   1883    var layers = {};
   1884    
   1885    /**
   1886     * All audio players currently in use by the client. Initially, this will
   1887     * be empty, but audio players may be allocated by the server upon request.
   1888     *
   1889     * @private
   1890     * @type {!Object.<number, Guacamole.AudioPlayer>}
   1891     */
   1892    var audioPlayers = {};
   1893
   1894    /**
   1895     * All video players currently in use by the client. Initially, this will
   1896     * be empty, but video players may be allocated by the server upon request.
   1897     *
   1898     * @private
   1899     * @type {!Object.<number, Guacamole.VideoPlayer>}
   1900     */
   1901    var videoPlayers = {};
   1902
   1903    // No initial parsers
   1904    var parsers = [];
   1905
   1906    // No initial streams 
   1907    var streams = [];
   1908
   1909    /**
   1910     * All current objects. The index of each object is dictated by the
   1911     * Guacamole server.
   1912     *
   1913     * @private
   1914     * @type {!Guacamole.Object[]}
   1915     */
   1916    var objects = [];
   1917
   1918    // Pool of available stream indices
   1919    var stream_indices = new Guacamole.IntegerPool();
   1920
   1921    // Array of allocated output streams by index
   1922    var output_streams = [];
   1923
   1924    function setState(state) {
   1925        if (state != currentState) {
   1926            currentState = state;
   1927            if (guac_client.onstatechange)
   1928                guac_client.onstatechange(currentState);
   1929        }
   1930    }
   1931
   1932    function isConnected() {
   1933        return currentState == Guacamole.Client.State.CONNECTED
   1934            || currentState == Guacamole.Client.State.WAITING;
   1935    }
   1936
   1937    /**
   1938     * Produces an opaque representation of Guacamole.Client state which can be
   1939     * later imported through a call to importState(). This object is
   1940     * effectively an independent, compressed snapshot of protocol and display
   1941     * state. Invoking this function implicitly flushes the display.
   1942     *
   1943     * @param {!function} callback
   1944     *     Callback which should be invoked once the state object is ready. The
   1945     *     state object will be passed to the callback as the sole parameter.
   1946     *     This callback may be invoked immediately, or later as the display
   1947     *     finishes rendering and becomes ready.
   1948     */
   1949    this.exportState = function exportState(callback) {
   1950
   1951        // Start with empty state
   1952        var state = {
   1953            'currentState' : currentState,
   1954            'currentTimestamp' : currentTimestamp,
   1955            'layers' : {}
   1956        };
   1957
   1958        var layersSnapshot = {};
   1959
   1960        // Make a copy of all current layers (protocol state)
   1961        for (var key in layers) {
   1962            layersSnapshot[key] = layers[key];
   1963        }
   1964
   1965        // Populate layers once data is available (display state, requires flush)
   1966        display.flush(function populateLayers() {
   1967
   1968            // Export each defined layer/buffer
   1969            for (var key in layersSnapshot) {
   1970
   1971                var index = parseInt(key);
   1972                var layer = layersSnapshot[key];
   1973                var canvas = layer.toCanvas();
   1974
   1975                // Store layer/buffer dimensions
   1976                var exportLayer = {
   1977                    'width'  : layer.width,
   1978                    'height' : layer.height
   1979                };
   1980
   1981                // Store layer/buffer image data, if it can be generated
   1982                if (layer.width && layer.height)
   1983                    exportLayer.url = canvas.toDataURL('image/png');
   1984
   1985                // Add layer properties if not a buffer nor the default layer
   1986                if (index > 0) {
   1987                    exportLayer.x = layer.x;
   1988                    exportLayer.y = layer.y;
   1989                    exportLayer.z = layer.z;
   1990                    exportLayer.alpha = layer.alpha;
   1991                    exportLayer.matrix = layer.matrix;
   1992                    exportLayer.parent = getLayerIndex(layer.parent);
   1993                }
   1994
   1995                // Store exported layer
   1996                state.layers[key] = exportLayer;
   1997
   1998            }
   1999
   2000            // Invoke callback now that the state is ready
   2001            callback(state);
   2002
   2003        });
   2004
   2005    };
   2006
   2007    /**
   2008     * Restores Guacamole.Client protocol and display state based on an opaque
   2009     * object from a prior call to exportState(). The Guacamole.Client instance
   2010     * used to export that state need not be the same as this instance.
   2011     *
   2012     * @param {!object} state
   2013     *     An opaque representation of Guacamole.Client state from a prior call
   2014     *     to exportState().
   2015     *
   2016     * @param {function} [callback]
   2017     *     The function to invoke when state has finished being imported. This
   2018     *     may happen immediately, or later as images within the provided state
   2019     *     object are loaded.
   2020     */
   2021    this.importState = function importState(state, callback) {
   2022
   2023        var key;
   2024        var index;
   2025
   2026        currentState = state.currentState;
   2027        currentTimestamp = state.currentTimestamp;
   2028
   2029        // Cancel any pending display operations/frames
   2030        display.cancel();
   2031
   2032        // Dispose of all layers
   2033        for (key in layers) {
   2034            index = parseInt(key);
   2035            if (index > 0)
   2036                layers[key].dispose();
   2037        }
   2038
   2039        layers = {};
   2040
   2041        // Import state of each layer/buffer
   2042        for (key in state.layers) {
   2043
   2044            index = parseInt(key);
   2045
   2046            var importLayer = state.layers[key];
   2047            var layer = getLayer(index);
   2048
   2049            // Reset layer size
   2050            display.resize(layer, importLayer.width, importLayer.height);
   2051
   2052            // Initialize new layer if it has associated data
   2053            if (importLayer.url) {
   2054                display.setChannelMask(layer, Guacamole.Layer.SRC);
   2055                display.draw(layer, 0, 0, importLayer.url);
   2056            }
   2057
   2058            // Set layer-specific properties if not a buffer nor the default layer
   2059            if (index > 0 && importLayer.parent >= 0) {
   2060
   2061                // Apply layer position and set parent
   2062                var parent = getLayer(importLayer.parent);
   2063                display.move(layer, parent, importLayer.x, importLayer.y, importLayer.z);
   2064
   2065                // Set layer transparency
   2066                display.shade(layer, importLayer.alpha);
   2067
   2068                // Apply matrix transform
   2069                var matrix = importLayer.matrix;
   2070                display.distort(layer,
   2071                    matrix[0], matrix[1], matrix[2],
   2072                    matrix[3], matrix[4], matrix[5]);
   2073
   2074            }
   2075
   2076        }
   2077
   2078        // Flush changes to display
   2079        display.flush(callback);
   2080
   2081    };
   2082
   2083    /**
   2084     * Returns the underlying display of this Guacamole.Client. The display
   2085     * contains an Element which can be added to the DOM, causing the
   2086     * display to become visible.
   2087     * 
   2088     * @return {!Guacamole.Display}
   2089     *     The underlying display of this Guacamole.Client.
   2090     */
   2091    this.getDisplay = function() {
   2092        return display;
   2093    };
   2094
   2095    /**
   2096     * Sends the current size of the screen.
   2097     * 
   2098     * @param {!number} width
   2099     *     The width of the screen.
   2100     *
   2101     * @param {!number} height
   2102     *     The height of the screen.
   2103     */
   2104    this.sendSize = function(width, height) {
   2105
   2106        // Do not send requests if not connected
   2107        if (!isConnected())
   2108            return;
   2109
   2110        tunnel.sendMessage("size", width, height);
   2111
   2112    };
   2113
   2114    /**
   2115     * Sends a key event having the given properties as if the user
   2116     * pressed or released a key.
   2117     * 
   2118     * @param {!boolean} pressed
   2119     *     Whether the key is pressed (true) or released (false).
   2120     *
   2121     * @param {!number} keysym
   2122     *     The keysym of the key being pressed or released.
   2123     */
   2124    this.sendKeyEvent = function(pressed, keysym) {
   2125        // Do not send requests if not connected
   2126        if (!isConnected())
   2127            return;
   2128
   2129        tunnel.sendMessage("key", keysym, pressed);
   2130    };
   2131
   2132    /**
   2133     * Sends a mouse event having the properties provided by the given mouse
   2134     * state.
   2135     * 
   2136     * @param {!Guacamole.Mouse.State} mouseState
   2137     *     The state of the mouse to send in the mouse event.
   2138     *
   2139     * @param {boolean} [applyDisplayScale=false]
   2140     *     Whether the provided mouse state uses local display units, rather
   2141     *     than remote display units, and should be scaled to match the
   2142     *     {@link Guacamole.Display}.
   2143     */
   2144    this.sendMouseState = function sendMouseState(mouseState, applyDisplayScale) {
   2145
   2146        // Do not send requests if not connected
   2147        if (!isConnected())
   2148            return;
   2149
   2150        var x = mouseState.x;
   2151        var y = mouseState.y;
   2152
   2153        // Translate for display units if requested
   2154        if (applyDisplayScale) {
   2155            x /= display.getScale();
   2156            y /= display.getScale();
   2157        }
   2158
   2159        // Update client-side cursor
   2160        display.moveCursor(
   2161            Math.floor(x),
   2162            Math.floor(y)
   2163        );
   2164
   2165        // Build mask
   2166        var buttonMask = 0;
   2167        if (mouseState.left)   buttonMask |= 1;
   2168        if (mouseState.middle) buttonMask |= 2;
   2169        if (mouseState.right)  buttonMask |= 4;
   2170        if (mouseState.up)     buttonMask |= 8;
   2171        if (mouseState.down)   buttonMask |= 16;
   2172
   2173        // Send message
   2174        tunnel.sendMessage("mouse", Math.floor(x), Math.floor(y), buttonMask);
   2175    };
   2176
   2177    /**
   2178     * Sends a touch event having the properties provided by the given touch
   2179     * state.
   2180     *
   2181     * @param {!Guacamole.Touch.State} touchState
   2182     *     The state of the touch contact to send in the touch event.
   2183     *
   2184     * @param {boolean} [applyDisplayScale=false]
   2185     *     Whether the provided touch state uses local display units, rather
   2186     *     than remote display units, and should be scaled to match the
   2187     *     {@link Guacamole.Display}.
   2188     */
   2189    this.sendTouchState = function sendTouchState(touchState, applyDisplayScale) {
   2190
   2191        // Do not send requests if not connected
   2192        if (!isConnected())
   2193            return;
   2194
   2195        var x = touchState.x;
   2196        var y = touchState.y;
   2197
   2198        // Translate for display units if requested
   2199        if (applyDisplayScale) {
   2200            x /= display.getScale();
   2201            y /= display.getScale();
   2202        }
   2203
   2204        tunnel.sendMessage('touch', touchState.id, Math.floor(x), Math.floor(y),
   2205            Math.floor(touchState.radiusX), Math.floor(touchState.radiusY),
   2206            touchState.angle, touchState.force);
   2207
   2208    };
   2209
   2210    /**
   2211     * Allocates an available stream index and creates a new
   2212     * Guacamole.OutputStream using that index, associating the resulting
   2213     * stream with this Guacamole.Client. Note that this stream will not yet
   2214     * exist as far as the other end of the Guacamole connection is concerned.
   2215     * Streams exist within the Guacamole protocol only when referenced by an
   2216     * instruction which creates the stream, such as a "clipboard", "file", or
   2217     * "pipe" instruction.
   2218     *
   2219     * @returns {!Guacamole.OutputStream}
   2220     *     A new Guacamole.OutputStream with a newly-allocated index and
   2221     *     associated with this Guacamole.Client.
   2222     */
   2223    this.createOutputStream = function createOutputStream() {
   2224
   2225        // Allocate index
   2226        var index = stream_indices.next();
   2227
   2228        // Return new stream
   2229        var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index);
   2230        return stream;
   2231
   2232    };
   2233
   2234    /**
   2235     * Opens a new audio stream for writing, where audio data having the give
   2236     * mimetype will be sent along the returned stream. The instruction
   2237     * necessary to create this stream will automatically be sent.
   2238     *
   2239     * @param {!string} mimetype
   2240     *     The mimetype of the audio data that will be sent along the returned
   2241     *     stream.
   2242     *
   2243     * @return {!Guacamole.OutputStream}
   2244     *     The created audio stream.
   2245     */
   2246    this.createAudioStream = function(mimetype) {
   2247
   2248        // Allocate and associate stream with audio metadata
   2249        var stream = guac_client.createOutputStream();
   2250        tunnel.sendMessage("audio", stream.index, mimetype);
   2251        return stream;
   2252
   2253    };
   2254
   2255    /**
   2256     * Opens a new file for writing, having the given index, mimetype and
   2257     * filename. The instruction necessary to create this stream will
   2258     * automatically be sent.
   2259     *
   2260     * @param {!string} mimetype
   2261     *     The mimetype of the file being sent.
   2262     *
   2263     * @param {!string} filename
   2264     *     The filename of the file being sent.
   2265     *
   2266     * @return {!Guacamole.OutputStream}
   2267     *     The created file stream.
   2268     */
   2269    this.createFileStream = function(mimetype, filename) {
   2270
   2271        // Allocate and associate stream with file metadata
   2272        var stream = guac_client.createOutputStream();
   2273        tunnel.sendMessage("file", stream.index, mimetype, filename);
   2274        return stream;
   2275
   2276    };
   2277
   2278    /**
   2279     * Opens a new pipe for writing, having the given name and mimetype. The
   2280     * instruction necessary to create this stream will automatically be sent.
   2281     *
   2282     * @param {!string} mimetype
   2283     *     The mimetype of the data being sent.
   2284     *
   2285     * @param {!string} name
   2286     *     The name of the pipe.
   2287     *
   2288     * @return {!Guacamole.OutputStream}
   2289     *     The created file stream.
   2290     */
   2291    this.createPipeStream = function(mimetype, name) {
   2292
   2293        // Allocate and associate stream with pipe metadata
   2294        var stream = guac_client.createOutputStream();
   2295        tunnel.sendMessage("pipe", stream.index, mimetype, name);
   2296        return stream;
   2297
   2298    };
   2299
   2300    /**
   2301     * Opens a new clipboard object for writing, having the given mimetype. The
   2302     * instruction necessary to create this stream will automatically be sent.
   2303     *
   2304     * @param {!string} mimetype
   2305     *     The mimetype of the data being sent.
   2306     *
   2307     * @param {!string} name
   2308     *     The name of the pipe.
   2309     *
   2310     * @return {!Guacamole.OutputStream}
   2311     *     The created file stream.
   2312     */
   2313    this.createClipboardStream = function(mimetype) {
   2314
   2315        // Allocate and associate stream with clipboard metadata
   2316        var stream = guac_client.createOutputStream();
   2317        tunnel.sendMessage("clipboard", stream.index, mimetype);
   2318        return stream;
   2319
   2320    };
   2321
   2322    /**
   2323     * Opens a new argument value stream for writing, having the given
   2324     * parameter name and mimetype, requesting that the connection parameter
   2325     * with the given name be updated to the value described by the contents
   2326     * of the following stream. The instruction necessary to create this stream
   2327     * will automatically be sent.
   2328     *
   2329     * @param {!string} mimetype
   2330     *     The mimetype of the data being sent.
   2331     *
   2332     * @param {!string} name
   2333     *     The name of the connection parameter to attempt to update.
   2334     *
   2335     * @return {!Guacamole.OutputStream}
   2336     *     The created argument value stream.
   2337     */
   2338    this.createArgumentValueStream = function createArgumentValueStream(mimetype, name) {
   2339
   2340        // Allocate and associate stream with argument value metadata
   2341        var stream = guac_client.createOutputStream();
   2342        tunnel.sendMessage("argv", stream.index, mimetype, name);
   2343        return stream;
   2344
   2345    };
   2346
   2347    /**
   2348     * Creates a new output stream associated with the given object and having
   2349     * the given mimetype and name. The legality of a mimetype and name is
   2350     * dictated by the object itself. The instruction necessary to create this
   2351     * stream will automatically be sent.
   2352     *
   2353     * @param {!number} index
   2354     *     The index of the object for which the output stream is being
   2355     *     created.
   2356     *
   2357     * @param {!string} mimetype
   2358     *     The mimetype of the data which will be sent to the output stream.
   2359     *
   2360     * @param {!string} name
   2361     *     The defined name of an output stream within the given object.
   2362     *
   2363     * @returns {!Guacamole.OutputStream}
   2364     *     An output stream which will write blobs to the named output stream
   2365     *     of the given object.
   2366     */
   2367    this.createObjectOutputStream = function createObjectOutputStream(index, mimetype, name) {
   2368
   2369        // Allocate and associate stream with object metadata
   2370        var stream = guac_client.createOutputStream();
   2371        tunnel.sendMessage("put", index, stream.index, mimetype, name);
   2372        return stream;
   2373
   2374    };
   2375
   2376    /**
   2377     * Requests read access to the input stream having the given name. If
   2378     * successful, a new input stream will be created.
   2379     *
   2380     * @param {!number} index
   2381     *     The index of the object from which the input stream is being
   2382     *     requested.
   2383     *
   2384     * @param {!string} name
   2385     *     The name of the input stream to request.
   2386     */
   2387    this.requestObjectInputStream = function requestObjectInputStream(index, name) {
   2388
   2389        // Do not send requests if not connected
   2390        if (!isConnected())
   2391            return;
   2392
   2393        tunnel.sendMessage("get", index, name);
   2394    };
   2395
   2396    /**
   2397     * Acknowledge receipt of a blob on the stream with the given index.
   2398     * 
   2399     * @param {!number} index
   2400     *     The index of the stream associated with the received blob.
   2401     *
   2402     * @param {!string} message
   2403     *     A human-readable message describing the error or status.
   2404     *
   2405     * @param {!number} code
   2406     *     The error code, if any, or 0 for success.
   2407     */
   2408    this.sendAck = function(index, message, code) {
   2409
   2410        // Do not send requests if not connected
   2411        if (!isConnected())
   2412            return;
   2413
   2414        tunnel.sendMessage("ack", index, message, code);
   2415    };
   2416
   2417    /**
   2418     * Given the index of a file, writes a blob of data to that file.
   2419     * 
   2420     * @param {!number} index
   2421     *     The index of the file to write to.
   2422     *
   2423     * @param {!string} data
   2424     *     Base64-encoded data to write to the file.
   2425     */
   2426    this.sendBlob = function(index, data) {
   2427
   2428        // Do not send requests if not connected
   2429        if (!isConnected())
   2430            return;
   2431
   2432        tunnel.sendMessage("blob", index, data);
   2433    };
   2434
   2435    /**
   2436     * Marks a currently-open stream as complete. The other end of the
   2437     * Guacamole connection will be notified via an "end" instruction that the
   2438     * stream is closed, and the index will be made available for reuse in
   2439     * future streams.
   2440     * 
   2441     * @param {!number} index
   2442     *     The index of the stream to end.
   2443     */
   2444    this.endStream = function(index) {
   2445
   2446        // Do not send requests if not connected
   2447        if (!isConnected())
   2448            return;
   2449
   2450        // Explicitly close stream by sending "end" instruction
   2451        tunnel.sendMessage("end", index);
   2452
   2453        // Free associated index and stream if they exist
   2454        if (output_streams[index]) {
   2455            stream_indices.free(index);
   2456            delete output_streams[index];
   2457        }
   2458
   2459    };
   2460
   2461    /**
   2462     * Fired whenever the state of this Guacamole.Client changes.
   2463     * 
   2464     * @event
   2465     * @param {!number} state
   2466     *     The new state of the client.
   2467     */
   2468    this.onstatechange = null;
   2469
   2470    /**
   2471     * Fired when the remote client sends a name update.
   2472     * 
   2473     * @event
   2474     * @param {!string} name
   2475     *     The new name of this client.
   2476     */
   2477    this.onname = null;
   2478
   2479    /**
   2480     * Fired when an error is reported by the remote client, and the connection
   2481     * is being closed.
   2482     * 
   2483     * @event
   2484     * @param {!Guacamole.Status} status
   2485     *     A status object which describes the error.
   2486     */
   2487    this.onerror = null;
   2488    
   2489    /**
   2490     * Fired when an arbitrary message is received from the tunnel that should
   2491     * be processed by the client. By default, additional message-specific
   2492     * events such as "onjoin" and "onleave" will fire for the received message
   2493     * after this event has been processed. An event handler for "onmsg" need
   2494     * not be supplied if "onjoin" and/or "onleave" will be used.
   2495     * 
   2496     * @event
   2497     * @param {!number} msgcode
   2498     *     A status code sent by the remote server that indicates the nature of
   2499     *     the message that is being sent to the client.
   2500     *     
   2501     * @param {string[]} args
   2502     *     An array of arguments to be processed with the message sent to the
   2503     *     client.
   2504     *
   2505     * @return {boolean}
   2506     *     true if message-specific events such as "onjoin" and
   2507     *     "onleave" should be fired for this message, false otherwise. If
   2508     *     no value is returned, message-specific events will be allowed to
   2509     *     fire.
   2510     */
   2511    this.onmsg = null;
   2512
   2513    /**
   2514     * Fired when a user joins a shared connection.
   2515     *
   2516     * @event
   2517     * @param {!string} userID
   2518     *     A unique value representing this specific user's connection to the
   2519     *     shared connection. This value is generated by the server and is
   2520     *     guaranteed to be unique relative to other users of the connection.
   2521     *
   2522     * @param {!string} name
   2523     *     A human-readable name representing the user that joined, such as
   2524     *     their username. This value is provided by the web application during
   2525     *     the connection handshake and is not necessarily unique relative to
   2526     *     other users of the connection.
   2527     */
   2528    this.onjoin = null;
   2529
   2530    /**
   2531     * Fired when a user leaves a shared connection.
   2532     *
   2533     * @event
   2534     * @param {!string} userID
   2535     *     A unique value representing this specific user's connection to the
   2536     *     shared connection. This value is generated by the server and is
   2537     *     guaranteed to be unique relative to other users of the connection.
   2538     *
   2539     * @param {!string} name
   2540     *     A human-readable name representing the user that left, such as their
   2541     *     username. This value is provided by the web application during the
   2542     *     connection handshake and is not necessarily unique relative to other
   2543     *     users of the connection.
   2544     */
   2545    this.onleave = null;
   2546
   2547    /**
   2548     * Fired when a audio stream is created. The stream provided to this event
   2549     * handler will contain its own event handlers for received data.
   2550     *
   2551     * @event
   2552     * @param {!Guacamole.InputStream} stream
   2553     *     The stream that will receive audio data from the server.
   2554     *
   2555     * @param {!string} mimetype
   2556     *     The mimetype of the audio data which will be received.
   2557     *
   2558     * @return {Guacamole.AudioPlayer}
   2559     *     An object which implements the Guacamole.AudioPlayer interface and
   2560     *     has been initialized to play the data in the provided stream, or null
   2561     *     if the built-in audio players of the Guacamole client should be
   2562     *     used.
   2563     */
   2564    this.onaudio = null;
   2565
   2566    /**
   2567     * Fired when a video stream is created. The stream provided to this event
   2568     * handler will contain its own event handlers for received data.
   2569     *
   2570     * @event
   2571     * @param {!Guacamole.InputStream} stream
   2572     *     The stream that will receive video data from the server.
   2573     *
   2574     * @param {!Guacamole.Display.VisibleLayer} layer
   2575     *     The destination layer on which the received video data should be
   2576     *     played. It is the responsibility of the Guacamole.VideoPlayer
   2577     *     implementation to play the received data within this layer.
   2578     *
   2579     * @param {!string} mimetype
   2580     *     The mimetype of the video data which will be received.
   2581     *
   2582     * @return {Guacamole.VideoPlayer}
   2583     *     An object which implements the Guacamole.VideoPlayer interface and
   2584     *     has been initialized to play the data in the provided stream, or null
   2585     *     if the built-in video players of the Guacamole client should be
   2586     *     used.
   2587     */
   2588    this.onvideo = null;
   2589
   2590    /**
   2591     * Fired when the remote client is explicitly declaring the level of
   2592     * multi-touch support provided by a particular display layer.
   2593     *
   2594     * @event
   2595     * @param {!Guacamole.Display.VisibleLayer} layer
   2596     *     The layer whose multi-touch support level is being declared.
   2597     *
   2598     * @param {!number} touches
   2599     *     The maximum number of simultaneous touches supported by the given
   2600     *     layer, where 0 indicates that touch events are not supported at all.
   2601     */
   2602    this.onmultitouch = null;
   2603
   2604    /**
   2605     * Fired when the current value of a connection parameter is being exposed
   2606     * by the server.
   2607     *
   2608     * @event
   2609     * @param {!Guacamole.InputStream} stream
   2610     *     The stream that will receive connection parameter data from the
   2611     *     server.
   2612     *
   2613     * @param {!string} mimetype
   2614     *     The mimetype of the data which will be received.
   2615     *
   2616     * @param {!string} name
   2617     *     The name of the connection parameter whose value is being exposed.
   2618     */
   2619    this.onargv = null;
   2620
   2621    /**
   2622     * Fired when the clipboard of the remote client is changing.
   2623     * 
   2624     * @event
   2625     * @param {!Guacamole.InputStream} stream
   2626     *     The stream that will receive clipboard data from the server.
   2627     *
   2628     * @param {!string} mimetype
   2629     *     The mimetype of the data which will be received.
   2630     */
   2631    this.onclipboard = null;
   2632
   2633    /**
   2634     * Fired when a file stream is created. The stream provided to this event
   2635     * handler will contain its own event handlers for received data.
   2636     * 
   2637     * @event
   2638     * @param {!Guacamole.InputStream} stream
   2639     *     The stream that will receive data from the server.
   2640     *
   2641     * @param {!string} mimetype
   2642     *     The mimetype of the file received.
   2643     *
   2644     * @param {!string} filename
   2645     *     The name of the file received.
   2646     */
   2647    this.onfile = null;
   2648
   2649    /**
   2650     * Fired when a filesystem object is created. The object provided to this
   2651     * event handler will contain its own event handlers and functions for
   2652     * requesting and handling data.
   2653     *
   2654     * @event
   2655     * @param {!Guacamole.Object} object
   2656     *     The created filesystem object.
   2657     *
   2658     * @param {!string} name
   2659     *     The name of the filesystem.
   2660     */
   2661    this.onfilesystem = null;
   2662
   2663    /**
   2664     * Fired when a pipe stream is created. The stream provided to this event
   2665     * handler will contain its own event handlers for received data;
   2666     * 
   2667     * @event
   2668     * @param {!Guacamole.InputStream} stream
   2669     *     The stream that will receive data from the server.
   2670     *
   2671     * @param {!string} mimetype
   2672     *     The mimetype of the data which will be received.
   2673     *
   2674     * @param {!string} name
   2675     *     The name of the pipe.
   2676     */
   2677    this.onpipe = null;
   2678    
   2679    /**
   2680     * Fired when a "required" instruction is received. A required instruction
   2681     * indicates that additional parameters are required for the connection to
   2682     * continue, such as user credentials.
   2683     * 
   2684     * @event
   2685     * @param {!string[]} parameters
   2686     *      The names of the connection parameters that are required to be
   2687     *      provided for the connection to continue.
   2688     */
   2689    this.onrequired = null;
   2690
   2691    /**
   2692     * Fired whenever a sync instruction is received from the server, indicating
   2693     * that the server is finished processing any input from the client and
   2694     * has sent any results.
   2695     * 
   2696     * @event
   2697     * @param {!number} timestamp
   2698     *     The timestamp associated with the sync instruction.
   2699     *
   2700     * @param {!number} frames
   2701     *     The number of frames that were considered or combined to produce the
   2702     *     frame associated with this sync instruction, or zero if this value
   2703     *     is not known or the remote desktop server provides no concept of
   2704     *     frames.
   2705     */
   2706    this.onsync = null;
   2707
   2708    /**
   2709     * Returns the layer with the given index, creating it if necessary.
   2710     * Positive indices refer to visible layers, an index of zero refers to
   2711     * the default layer, and negative indices refer to buffers.
   2712     *
   2713     * @private
   2714     * @param {!number} index
   2715     *     The index of the layer to retrieve.
   2716     *
   2717     * @return {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)}
   2718     *     The layer having the given index.
   2719     */
   2720    var getLayer = function getLayer(index) {
   2721
   2722        // Get layer, create if necessary
   2723        var layer = layers[index];
   2724        if (!layer) {
   2725
   2726            // Create layer based on index
   2727            if (index === 0)
   2728                layer = display.getDefaultLayer();
   2729            else if (index > 0)
   2730                layer = display.createLayer();
   2731            else
   2732                layer = display.createBuffer();
   2733                
   2734            // Add new layer
   2735            layers[index] = layer;
   2736
   2737        }
   2738
   2739        return layer;
   2740
   2741    };
   2742
   2743    /**
   2744     * Returns the index passed to getLayer() when the given layer was created.
   2745     * Positive indices refer to visible layers, an index of zero refers to the
   2746     * default layer, and negative indices refer to buffers.
   2747     *
   2748     * @param {!(Guacamole.Display.VisibleLayer|Guacamole.Layer)} layer
   2749     *     The layer whose index should be determined.
   2750     *
   2751     * @returns {number}
   2752     *     The index of the given layer, or null if no such layer is associated
   2753     *     with this client.
   2754     */
   2755    var getLayerIndex = function getLayerIndex(layer) {
   2756
   2757        // Avoid searching if there clearly is no such layer
   2758        if (!layer)
   2759            return null;
   2760
   2761        // Search through each layer, returning the index of the given layer
   2762        // once found
   2763        for (var key in layers) {
   2764            if (layer === layers[key])
   2765                return parseInt(key);
   2766        }
   2767
   2768        // Otherwise, no such index
   2769        return null;
   2770
   2771    };
   2772
   2773    function getParser(index) {
   2774
   2775        var parser = parsers[index];
   2776
   2777        // If parser not yet created, create it, and tie to the
   2778        // oninstruction handler of the tunnel.
   2779        if (parser == null) {
   2780            parser = parsers[index] = new Guacamole.Parser();
   2781            parser.oninstruction = tunnel.oninstruction;
   2782        }
   2783
   2784        return parser;
   2785
   2786    }
   2787
   2788    /**
   2789     * Handlers for all defined layer properties.
   2790     *
   2791     * @private
   2792     * @type {!Object.<string, function>}
   2793     */
   2794    var layerPropertyHandlers = {
   2795
   2796        "miter-limit": function(layer, value) {
   2797            display.setMiterLimit(layer, parseFloat(value));
   2798        },
   2799
   2800        "multi-touch" : function layerSupportsMultiTouch(layer, value) {
   2801
   2802            // Process "multi-touch" property only for true visible layers (not off-screen buffers)
   2803            if (guac_client.onmultitouch && layer instanceof Guacamole.Display.VisibleLayer)
   2804                guac_client.onmultitouch(layer, parseInt(value));
   2805
   2806        }
   2807
   2808    };
   2809    
   2810    /**
   2811     * Handlers for all instruction opcodes receivable by a Guacamole protocol
   2812     * client.
   2813     *
   2814     * @private
   2815     * @type {!Object.<string, function>}
   2816     */
   2817    var instructionHandlers = {
   2818
   2819        "ack": function(parameters) {
   2820
   2821            var stream_index = parseInt(parameters[0]);
   2822            var reason = parameters[1];
   2823            var code = parseInt(parameters[2]);
   2824
   2825            // Get stream
   2826            var stream = output_streams[stream_index];
   2827            if (stream) {
   2828
   2829                // Signal ack if handler defined
   2830                if (stream.onack)
   2831                    stream.onack(new Guacamole.Status(code, reason));
   2832
   2833                // If code is an error, invalidate stream if not already
   2834                // invalidated by onack handler
   2835                if (code >= 0x0100 && output_streams[stream_index] === stream) {
   2836                    stream_indices.free(stream_index);
   2837                    delete output_streams[stream_index];
   2838                }
   2839
   2840            }
   2841
   2842        },
   2843
   2844        "arc": function(parameters) {
   2845
   2846            var layer = getLayer(parseInt(parameters[0]));
   2847            var x = parseInt(parameters[1]);
   2848            var y = parseInt(parameters[2]);
   2849            var radius = parseInt(parameters[3]);
   2850            var startAngle = parseFloat(parameters[4]);
   2851            var endAngle = parseFloat(parameters[5]);
   2852            var negative = parseInt(parameters[6]);
   2853
   2854            display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0);
   2855
   2856        },
   2857
   2858        "argv": function(parameters) {
   2859
   2860            var stream_index = parseInt(parameters[0]);
   2861            var mimetype = parameters[1];
   2862            var name = parameters[2];
   2863
   2864            // Create stream
   2865            if (guac_client.onargv) {
   2866                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
   2867                guac_client.onargv(stream, mimetype, name);
   2868            }
   2869
   2870            // Otherwise, unsupported
   2871            else
   2872                guac_client.sendAck(stream_index, "Receiving argument values unsupported", 0x0100);
   2873
   2874        },
   2875
   2876        "audio": function(parameters) {
   2877
   2878            var stream_index = parseInt(parameters[0]);
   2879            var mimetype = parameters[1];
   2880
   2881            // Create stream 
   2882            var stream = streams[stream_index] =
   2883                    new Guacamole.InputStream(guac_client, stream_index);
   2884
   2885            // Get player instance via callback
   2886            var audioPlayer = null;
   2887            if (guac_client.onaudio)
   2888                audioPlayer = guac_client.onaudio(stream, mimetype);
   2889
   2890            // If unsuccessful, try to use a default implementation
   2891            if (!audioPlayer)
   2892                audioPlayer = Guacamole.AudioPlayer.getInstance(stream, mimetype);
   2893
   2894            // If we have successfully retrieved an audio player, send success response
   2895            if (audioPlayer) {
   2896                audioPlayers[stream_index] = audioPlayer;
   2897                guac_client.sendAck(stream_index, "OK", 0x0000);
   2898            }
   2899
   2900            // Otherwise, mimetype must be unsupported
   2901            else
   2902                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
   2903
   2904        },
   2905
   2906        "blob": function(parameters) {
   2907
   2908            // Get stream 
   2909            var stream_index = parseInt(parameters[0]);
   2910            var data = parameters[1];
   2911            var stream = streams[stream_index];
   2912
   2913            // Write data
   2914            if (stream && stream.onblob)
   2915                stream.onblob(data);
   2916
   2917        },
   2918
   2919        "body" : function handleBody(parameters) {
   2920
   2921            // Get object
   2922            var objectIndex = parseInt(parameters[0]);
   2923            var object = objects[objectIndex];
   2924
   2925            var streamIndex = parseInt(parameters[1]);
   2926            var mimetype = parameters[2];
   2927            var name = parameters[3];
   2928
   2929            // Create stream if handler defined
   2930            if (object && object.onbody) {
   2931                var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex);
   2932                object.onbody(stream, mimetype, name);
   2933            }
   2934
   2935            // Otherwise, unsupported
   2936            else
   2937                guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100);
   2938
   2939        },
   2940
   2941        "cfill": function(parameters) {
   2942
   2943            var channelMask = parseInt(parameters[0]);
   2944            var layer = getLayer(parseInt(parameters[1]));
   2945            var r = parseInt(parameters[2]);
   2946            var g = parseInt(parameters[3]);
   2947            var b = parseInt(parameters[4]);
   2948            var a = parseInt(parameters[5]);
   2949
   2950            display.setChannelMask(layer, channelMask);
   2951            display.fillColor(layer, r, g, b, a);
   2952
   2953        },
   2954
   2955        "clip": function(parameters) {
   2956
   2957            var layer = getLayer(parseInt(parameters[0]));
   2958
   2959            display.clip(layer);
   2960
   2961        },
   2962
   2963        "clipboard": function(parameters) {
   2964
   2965            var stream_index = parseInt(parameters[0]);
   2966            var mimetype = parameters[1];
   2967
   2968            // Create stream 
   2969            if (guac_client.onclipboard) {
   2970                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
   2971                guac_client.onclipboard(stream, mimetype);
   2972            }
   2973
   2974            // Otherwise, unsupported
   2975            else
   2976                guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100);
   2977
   2978        },
   2979
   2980        "close": function(parameters) {
   2981
   2982            var layer = getLayer(parseInt(parameters[0]));
   2983
   2984            display.close(layer);
   2985
   2986        },
   2987
   2988        "copy": function(parameters) {
   2989
   2990            var srcL = getLayer(parseInt(parameters[0]));
   2991            var srcX = parseInt(parameters[1]);
   2992            var srcY = parseInt(parameters[2]);
   2993            var srcWidth = parseInt(parameters[3]);
   2994            var srcHeight = parseInt(parameters[4]);
   2995            var channelMask = parseInt(parameters[5]);
   2996            var dstL = getLayer(parseInt(parameters[6]));
   2997            var dstX = parseInt(parameters[7]);
   2998            var dstY = parseInt(parameters[8]);
   2999
   3000            display.setChannelMask(dstL, channelMask);
   3001            display.copy(srcL, srcX, srcY, srcWidth, srcHeight, 
   3002                         dstL, dstX, dstY);
   3003
   3004        },
   3005
   3006        "cstroke": function(parameters) {
   3007
   3008            var channelMask = parseInt(parameters[0]);
   3009            var layer = getLayer(parseInt(parameters[1]));
   3010            var cap = lineCap[parseInt(parameters[2])];
   3011            var join = lineJoin[parseInt(parameters[3])];
   3012            var thickness = parseInt(parameters[4]);
   3013            var r = parseInt(parameters[5]);
   3014            var g = parseInt(parameters[6]);
   3015            var b = parseInt(parameters[7]);
   3016            var a = parseInt(parameters[8]);
   3017
   3018            display.setChannelMask(layer, channelMask);
   3019            display.strokeColor(layer, cap, join, thickness, r, g, b, a);
   3020
   3021        },
   3022
   3023        "cursor": function(parameters) {
   3024
   3025            var cursorHotspotX = parseInt(parameters[0]);
   3026            var cursorHotspotY = parseInt(parameters[1]);
   3027            var srcL = getLayer(parseInt(parameters[2]));
   3028            var srcX = parseInt(parameters[3]);
   3029            var srcY = parseInt(parameters[4]);
   3030            var srcWidth = parseInt(parameters[5]);
   3031            var srcHeight = parseInt(parameters[6]);
   3032
   3033            display.setCursor(cursorHotspotX, cursorHotspotY,
   3034                              srcL, srcX, srcY, srcWidth, srcHeight);
   3035
   3036        },
   3037
   3038        "curve": function(parameters) {
   3039
   3040            var layer = getLayer(parseInt(parameters[0]));
   3041            var cp1x = parseInt(parameters[1]);
   3042            var cp1y = parseInt(parameters[2]);
   3043            var cp2x = parseInt(parameters[3]);
   3044            var cp2y = parseInt(parameters[4]);
   3045            var x = parseInt(parameters[5]);
   3046            var y = parseInt(parameters[6]);
   3047
   3048            display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y);
   3049
   3050        },
   3051
   3052        "disconnect" : function handleDisconnect(parameters) {
   3053
   3054            // Explicitly tear down connection
   3055            guac_client.disconnect();
   3056
   3057        },
   3058
   3059        "dispose": function(parameters) {
   3060            
   3061            var layer_index = parseInt(parameters[0]);
   3062
   3063            // If visible layer, remove from parent
   3064            if (layer_index > 0) {
   3065
   3066                // Remove from parent
   3067                var layer = getLayer(layer_index);
   3068                display.dispose(layer);
   3069
   3070                // Delete reference
   3071                delete layers[layer_index];
   3072
   3073            }
   3074
   3075            // If buffer, just delete reference
   3076            else if (layer_index < 0)
   3077                delete layers[layer_index];
   3078
   3079            // Attempting to dispose the root layer currently has no effect.
   3080
   3081        },
   3082
   3083        "distort": function(parameters) {
   3084
   3085            var layer_index = parseInt(parameters[0]);
   3086            var a = parseFloat(parameters[1]);
   3087            var b = parseFloat(parameters[2]);
   3088            var c = parseFloat(parameters[3]);
   3089            var d = parseFloat(parameters[4]);
   3090            var e = parseFloat(parameters[5]);
   3091            var f = parseFloat(parameters[6]);
   3092
   3093            // Only valid for visible layers (not buffers)
   3094            if (layer_index >= 0) {
   3095                var layer = getLayer(layer_index);
   3096                display.distort(layer, a, b, c, d, e, f);
   3097            }
   3098
   3099        },
   3100 
   3101        "error": function(parameters) {
   3102
   3103            var reason = parameters[0];
   3104            var code = parseInt(parameters[1]);
   3105
   3106            // Call handler if defined
   3107            if (guac_client.onerror)
   3108                guac_client.onerror(new Guacamole.Status(code, reason));
   3109
   3110            guac_client.disconnect();
   3111
   3112        },
   3113
   3114        "end": function(parameters) {
   3115
   3116            var stream_index = parseInt(parameters[0]);
   3117
   3118            // Get stream
   3119            var stream = streams[stream_index];
   3120            if (stream) {
   3121
   3122                // Signal end of stream if handler defined
   3123                if (stream.onend)
   3124                    stream.onend();
   3125
   3126                // Invalidate stream
   3127                delete streams[stream_index];
   3128
   3129            }
   3130
   3131        },
   3132
   3133        "file": function(parameters) {
   3134
   3135            var stream_index = parseInt(parameters[0]);
   3136            var mimetype = parameters[1];
   3137            var filename = parameters[2];
   3138
   3139            // Create stream 
   3140            if (guac_client.onfile) {
   3141                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
   3142                guac_client.onfile(stream, mimetype, filename);
   3143            }
   3144
   3145            // Otherwise, unsupported
   3146            else
   3147                guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100);
   3148
   3149        },
   3150
   3151        "filesystem" : function handleFilesystem(parameters) {
   3152
   3153            var objectIndex = parseInt(parameters[0]);
   3154            var name = parameters[1];
   3155
   3156            // Create object, if supported
   3157            if (guac_client.onfilesystem) {
   3158                var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex);
   3159                guac_client.onfilesystem(object, name);
   3160            }
   3161
   3162            // If unsupported, simply ignore the availability of the filesystem
   3163
   3164        },
   3165
   3166        "identity": function(parameters) {
   3167
   3168            var layer = getLayer(parseInt(parameters[0]));
   3169
   3170            display.setTransform(layer, 1, 0, 0, 1, 0, 0);
   3171
   3172        },
   3173
   3174        "img": function(parameters) {
   3175
   3176            var stream_index = parseInt(parameters[0]);
   3177            var channelMask = parseInt(parameters[1]);
   3178            var layer = getLayer(parseInt(parameters[2]));
   3179            var mimetype = parameters[3];
   3180            var x = parseInt(parameters[4]);
   3181            var y = parseInt(parameters[5]);
   3182
   3183            // Create stream
   3184            var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
   3185
   3186            // Draw received contents once decoded
   3187            display.setChannelMask(layer, channelMask);
   3188            display.drawStream(layer, x, y, stream, mimetype);
   3189
   3190        },
   3191
   3192        "jpeg": function(parameters) {
   3193
   3194            var channelMask = parseInt(parameters[0]);
   3195            var layer = getLayer(parseInt(parameters[1]));
   3196            var x = parseInt(parameters[2]);
   3197            var y = parseInt(parameters[3]);
   3198            var data = parameters[4];
   3199
   3200            display.setChannelMask(layer, channelMask);
   3201            display.draw(layer, x, y, "data:image/jpeg;base64," + data);
   3202
   3203        },
   3204
   3205        "lfill": function(parameters) {
   3206
   3207            var channelMask = parseInt(parameters[0]);
   3208            var layer = getLayer(parseInt(parameters[1]));
   3209            var srcLayer = getLayer(parseInt(parameters[2]));
   3210
   3211            display.setChannelMask(layer, channelMask);
   3212            display.fillLayer(layer, srcLayer);
   3213
   3214        },
   3215
   3216        "line": function(parameters) {
   3217
   3218            var layer = getLayer(parseInt(parameters[0]));
   3219            var x = parseInt(parameters[1]);
   3220            var y = parseInt(parameters[2]);
   3221
   3222            display.lineTo(layer, x, y);
   3223
   3224        },
   3225
   3226        "lstroke": function(parameters) {
   3227
   3228            var channelMask = parseInt(parameters[0]);
   3229            var layer = getLayer(parseInt(parameters[1]));
   3230            var srcLayer = getLayer(parseInt(parameters[2]));
   3231
   3232            display.setChannelMask(layer, channelMask);
   3233            display.strokeLayer(layer, srcLayer);
   3234
   3235        },
   3236
   3237        "mouse" : function handleMouse(parameters) {
   3238
   3239            var x = parseInt(parameters[0]);
   3240            var y = parseInt(parameters[1]);
   3241
   3242            // Display and move software cursor to received coordinates
   3243            display.showCursor(true);
   3244            display.moveCursor(x, y);
   3245
   3246        },
   3247
   3248        "move": function(parameters) {
   3249            
   3250            var layer_index = parseInt(parameters[0]);
   3251            var parent_index = parseInt(parameters[1]);
   3252            var x = parseInt(parameters[2]);
   3253            var y = parseInt(parameters[3]);
   3254            var z = parseInt(parameters[4]);
   3255
   3256            // Only valid for non-default layers
   3257            if (layer_index > 0 && parent_index >= 0) {
   3258                var layer = getLayer(layer_index);
   3259                var parent = getLayer(parent_index);
   3260                display.move(layer, parent, x, y, z);
   3261            }
   3262
   3263        },
   3264        
   3265        "msg" : function(parameters) {
   3266
   3267            var userID;
   3268            var username;
   3269
   3270            // Fire general message handling event first
   3271            var allowDefault = true;
   3272            var msgid = parseInt(parameters[0]);
   3273            if (guac_client.onmsg) {
   3274                allowDefault = guac_client.onmsg(msgid, parameters.slice(1));
   3275                if (allowDefault === undefined)
   3276                    allowDefault = true;
   3277            }
   3278
   3279            // Fire message-specific convenience events if not prevented by the
   3280            // "onmsg" handler
   3281            if (allowDefault) {
   3282                switch (msgid) {
   3283
   3284                    case Guacamole.Client.Message.USER_JOINED:
   3285                        userID = parameters[1];
   3286                        username = parameters[2];
   3287                        if (guac_client.onjoin)
   3288                            guac_client.onjoin(userID, username);
   3289                        break;
   3290
   3291                    case Guacamole.Client.Message.USER_LEFT:
   3292                        userID = parameters[1];
   3293                        username = parameters[2];
   3294                        if (guac_client.onleave)
   3295                            guac_client.onleave(userID, username);
   3296                        break;
   3297
   3298                }
   3299            }
   3300            
   3301        },
   3302
   3303        "name": function(parameters) {
   3304            if (guac_client.onname) guac_client.onname(parameters[0]);
   3305        },
   3306
   3307        "nest": function(parameters) {
   3308            var parser = getParser(parseInt(parameters[0]));
   3309            parser.receive(parameters[1]);
   3310        },
   3311
   3312        "pipe": function(parameters) {
   3313
   3314            var stream_index = parseInt(parameters[0]);
   3315            var mimetype = parameters[1];
   3316            var name = parameters[2];
   3317
   3318            // Create stream 
   3319            if (guac_client.onpipe) {
   3320                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
   3321                guac_client.onpipe(stream, mimetype, name);
   3322            }
   3323
   3324            // Otherwise, unsupported
   3325            else
   3326                guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100);
   3327
   3328        },
   3329
   3330        "png": function(parameters) {
   3331
   3332            var channelMask = parseInt(parameters[0]);
   3333            var layer = getLayer(parseInt(parameters[1]));
   3334            var x = parseInt(parameters[2]);
   3335            var y = parseInt(parameters[3]);
   3336            var data = parameters[4];
   3337
   3338            display.setChannelMask(layer, channelMask);
   3339            display.draw(layer, x, y, "data:image/png;base64," + data);
   3340
   3341        },
   3342
   3343        "pop": function(parameters) {
   3344
   3345            var layer = getLayer(parseInt(parameters[0]));
   3346
   3347            display.pop(layer);
   3348
   3349        },
   3350
   3351        "push": function(parameters) {
   3352
   3353            var layer = getLayer(parseInt(parameters[0]));
   3354
   3355            display.push(layer);
   3356
   3357        },
   3358 
   3359        "rect": function(parameters) {
   3360
   3361            var layer = getLayer(parseInt(parameters[0]));
   3362            var x = parseInt(parameters[1]);
   3363            var y = parseInt(parameters[2]);
   3364            var w = parseInt(parameters[3]);
   3365            var h = parseInt(parameters[4]);
   3366
   3367            display.rect(layer, x, y, w, h);
   3368
   3369        },
   3370                
   3371        "required": function required(parameters) {
   3372            if (guac_client.onrequired) guac_client.onrequired(parameters);
   3373        },
   3374        
   3375        "reset": function(parameters) {
   3376
   3377            var layer = getLayer(parseInt(parameters[0]));
   3378
   3379            display.reset(layer);
   3380
   3381        },
   3382        
   3383        "set": function(parameters) {
   3384
   3385            var layer = getLayer(parseInt(parameters[0]));
   3386            var name = parameters[1];
   3387            var value = parameters[2];
   3388
   3389            // Call property handler if defined
   3390            var handler = layerPropertyHandlers[name];
   3391            if (handler)
   3392                handler(layer, value);
   3393
   3394        },
   3395
   3396        "shade": function(parameters) {
   3397            
   3398            var layer_index = parseInt(parameters[0]);
   3399            var a = parseInt(parameters[1]);
   3400
   3401            // Only valid for visible layers (not buffers)
   3402            if (layer_index >= 0) {
   3403                var layer = getLayer(layer_index);
   3404                display.shade(layer, a);
   3405            }
   3406
   3407        },
   3408
   3409        "size": function(parameters) {
   3410
   3411            var layer_index = parseInt(parameters[0]);
   3412            var layer = getLayer(layer_index);
   3413            var width = parseInt(parameters[1]);
   3414            var height = parseInt(parameters[2]);
   3415
   3416            display.resize(layer, width, height);
   3417
   3418        },
   3419        
   3420        "start": function(parameters) {
   3421
   3422            var layer = getLayer(parseInt(parameters[0]));
   3423            var x = parseInt(parameters[1]);
   3424            var y = parseInt(parameters[2]);
   3425
   3426            display.moveTo(layer, x, y);
   3427
   3428        },
   3429
   3430        "sync": function(parameters) {
   3431
   3432            var timestamp = parseInt(parameters[0]);
   3433            var frames = parameters[1] ? parseInt(parameters[1]) : 0;
   3434
   3435            // Flush display, send sync when done
   3436            display.flush(function displaySyncComplete() {
   3437
   3438                // Synchronize all audio players
   3439                for (var index in audioPlayers) {
   3440                    var audioPlayer = audioPlayers[index];
   3441                    if (audioPlayer)
   3442                        audioPlayer.sync();
   3443                }
   3444
   3445                // Send sync response to server
   3446                if (timestamp !== currentTimestamp) {
   3447                    tunnel.sendMessage("sync", timestamp);
   3448                    currentTimestamp = timestamp;
   3449                }
   3450
   3451            }, timestamp, frames);
   3452
   3453            // If received first update, no longer waiting.
   3454            if (currentState === Guacamole.Client.State.WAITING)
   3455                setState(Guacamole.Client.State.CONNECTED);
   3456
   3457            // Call sync handler if defined
   3458            if (guac_client.onsync)
   3459                guac_client.onsync(timestamp, frames);
   3460
   3461        },
   3462
   3463        "transfer": function(parameters) {
   3464
   3465            var srcL = getLayer(parseInt(parameters[0]));
   3466            var srcX = parseInt(parameters[1]);
   3467            var srcY = parseInt(parameters[2]);
   3468            var srcWidth = parseInt(parameters[3]);
   3469            var srcHeight = parseInt(parameters[4]);
   3470            var function_index = parseInt(parameters[5]);
   3471            var dstL = getLayer(parseInt(parameters[6]));
   3472            var dstX = parseInt(parameters[7]);
   3473            var dstY = parseInt(parameters[8]);
   3474
   3475            /* SRC */
   3476            if (function_index === 0x3)
   3477                display.put(srcL, srcX, srcY, srcWidth, srcHeight, 
   3478                    dstL, dstX, dstY);
   3479
   3480            /* Anything else that isn't a NO-OP */
   3481            else if (function_index !== 0x5)
   3482                display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, 
   3483                    dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]);
   3484
   3485        },
   3486
   3487        "transform": function(parameters) {
   3488
   3489            var layer = getLayer(parseInt(parameters[0]));
   3490            var a = parseFloat(parameters[1]);
   3491            var b = parseFloat(parameters[2]);
   3492            var c = parseFloat(parameters[3]);
   3493            var d = parseFloat(parameters[4]);
   3494            var e = parseFloat(parameters[5]);
   3495            var f = parseFloat(parameters[6]);
   3496
   3497            display.transform(layer, a, b, c, d, e, f);
   3498
   3499        },
   3500
   3501        "undefine" : function handleUndefine(parameters) {
   3502
   3503            // Get object
   3504            var objectIndex = parseInt(parameters[0]);
   3505            var object = objects[objectIndex];
   3506
   3507            // Signal end of object definition
   3508            if (object && object.onundefine)
   3509                object.onundefine();
   3510
   3511        },
   3512
   3513        "video": function(parameters) {
   3514
   3515            var stream_index = parseInt(parameters[0]);
   3516            var layer = getLayer(parseInt(parameters[1]));
   3517            var mimetype = parameters[2];
   3518
   3519            // Create stream
   3520            var stream = streams[stream_index] =
   3521                    new Guacamole.InputStream(guac_client, stream_index);
   3522
   3523            // Get player instance via callback
   3524            var videoPlayer = null;
   3525            if (guac_client.onvideo)
   3526                videoPlayer = guac_client.onvideo(stream, layer, mimetype);
   3527
   3528            // If unsuccessful, try to use a default implementation
   3529            if (!videoPlayer)
   3530                videoPlayer = Guacamole.VideoPlayer.getInstance(stream, layer, mimetype);
   3531
   3532            // If we have successfully retrieved an video player, send success response
   3533            if (videoPlayer) {
   3534                videoPlayers[stream_index] = videoPlayer;
   3535                guac_client.sendAck(stream_index, "OK", 0x0000);
   3536            }
   3537
   3538            // Otherwise, mimetype must be unsupported
   3539            else
   3540                guac_client.sendAck(stream_index, "BAD TYPE", 0x030F);
   3541
   3542        }
   3543
   3544    };
   3545
   3546    /**
   3547     * Sends a keep-alive ping to the Guacamole server, advising the server
   3548     * that the client is still connected and responding. The lastSentKeepAlive
   3549     * timestamp is automatically updated as a result of calling this function.
   3550     *
   3551     * @private
   3552     */
   3553    var sendKeepAlive = function sendKeepAlive() {
   3554        tunnel.sendMessage('nop');
   3555        lastSentKeepAlive = new Date().getTime();
   3556    };
   3557
   3558    /**
   3559     * Schedules the next keep-alive ping based on the KEEP_ALIVE_FREQUENCY and
   3560     * the time that the last ping was sent, if ever. If enough time has
   3561     * elapsed that a ping should have already been sent, calling this function
   3562     * will send that ping immediately.
   3563     *
   3564     * @private
   3565     */
   3566    var scheduleKeepAlive = function scheduleKeepAlive() {
   3567
   3568        window.clearTimeout(keepAliveTimeout);
   3569
   3570        var currentTime = new Date().getTime();
   3571        var keepAliveDelay = Math.max(lastSentKeepAlive + KEEP_ALIVE_FREQUENCY - currentTime, 0);
   3572
   3573        // Ping server regularly to keep connection alive, but send the ping
   3574        // immediately if enough time has elapsed that it should have already
   3575        // been sent
   3576        if (keepAliveDelay > 0)
   3577            keepAliveTimeout = window.setTimeout(sendKeepAlive, keepAliveDelay);
   3578        else
   3579            sendKeepAlive();
   3580
   3581    };
   3582
   3583    /**
   3584     * Stops sending any further keep-alive pings. If a keep-alive ping was
   3585     * scheduled to be sent, that ping is cancelled.
   3586     *
   3587     * @private
   3588     */
   3589    var stopKeepAlive = function stopKeepAlive() {
   3590        window.clearTimeout(keepAliveTimeout);
   3591    };
   3592
   3593    tunnel.oninstruction = function(opcode, parameters) {
   3594
   3595        var handler = instructionHandlers[opcode];
   3596        if (handler)
   3597            handler(parameters);
   3598
   3599        // Leverage network activity to ensure the next keep-alive ping is
   3600        // sent, even if the browser is currently throttling timers
   3601        scheduleKeepAlive();
   3602
   3603    };
   3604
   3605    /**
   3606     * Sends a disconnect instruction to the server and closes the tunnel.
   3607     */
   3608    this.disconnect = function() {
   3609
   3610        // Only attempt disconnection not disconnected.
   3611        if (currentState != Guacamole.Client.State.DISCONNECTED
   3612                && currentState != Guacamole.Client.State.DISCONNECTING) {
   3613
   3614            setState(Guacamole.Client.State.DISCONNECTING);
   3615
   3616            // Stop sending keep-alive messages
   3617            stopKeepAlive();
   3618
   3619            // Send disconnect message and disconnect
   3620            tunnel.sendMessage("disconnect");
   3621            tunnel.disconnect();
   3622            setState(Guacamole.Client.State.DISCONNECTED);
   3623
   3624        }
   3625
   3626    };
   3627    
   3628    /**
   3629     * Connects the underlying tunnel of this Guacamole.Client, passing the
   3630     * given arbitrary data to the tunnel during the connection process.
   3631     *
   3632     * @param {string} data
   3633     *     Arbitrary connection data to be sent to the underlying tunnel during
   3634     *     the connection process.
   3635     *
   3636     * @throws {!Guacamole.Status}
   3637     *     If an error occurs during connection.
   3638     */
   3639    this.connect = function(data) {
   3640
   3641        setState(Guacamole.Client.State.CONNECTING);
   3642
   3643        try {
   3644            tunnel.connect(data);
   3645        }
   3646        catch (status) {
   3647            setState(Guacamole.Client.State.IDLE);
   3648            throw status;
   3649        }
   3650
   3651        // Regularly send keep-alive ping to ensure the server knows we're
   3652        // still here, even if not active
   3653        scheduleKeepAlive();
   3654
   3655        setState(Guacamole.Client.State.WAITING);
   3656    };
   3657
   3658};
   3659
   3660/**
   3661 * All possible Guacamole Client states.
   3662 * 
   3663 * @type {!Object.<string, number>}
   3664 */
   3665Guacamole.Client.State = {
   3666    
   3667    /**
   3668     * The client is idle, with no active connection.
   3669     * 
   3670     * @type number
   3671     */
   3672    "IDLE" : 0,
   3673    
   3674    /**
   3675     * The client is in the process of establishing a connection.
   3676     * 
   3677     * @type {!number}
   3678     */
   3679    "CONNECTING" : 1,
   3680    
   3681    /**
   3682     * The client is waiting on further information or a remote server to
   3683     * establish the connection.
   3684     * 
   3685     * @type {!number}
   3686     */
   3687    "WAITING" : 2,
   3688    
   3689    /**
   3690     * The client is actively connected to a remote server.
   3691     * 
   3692     * @type {!number}
   3693     */
   3694    "CONNECTED" : 3,
   3695    
   3696    /**
   3697     * The client is in the process of disconnecting from the remote server.
   3698     * 
   3699     * @type {!number}
   3700     */
   3701    "DISCONNECTING" : 4,
   3702    
   3703    /**
   3704     * The client has completed the connection and is no longer connected.
   3705     * 
   3706     * @type {!number}
   3707     */
   3708    "DISCONNECTED" : 5
   3709    
   3710};
   3711
   3712/**
   3713 * Map of all Guacamole binary raster operations to transfer functions.
   3714 *
   3715 * @private
   3716 * @type {!Object.<number, function>}
   3717 */
   3718Guacamole.Client.DefaultTransferFunction = {
   3719
   3720    /* BLACK */
   3721    0x0: function (src, dst) {
   3722        dst.red = dst.green = dst.blue = 0x00;
   3723    },
   3724
   3725    /* WHITE */
   3726    0xF: function (src, dst) {
   3727        dst.red = dst.green = dst.blue = 0xFF;
   3728    },
   3729
   3730    /* SRC */
   3731    0x3: function (src, dst) {
   3732        dst.red   = src.red;
   3733        dst.green = src.green;
   3734        dst.blue  = src.blue;
   3735        dst.alpha = src.alpha;
   3736    },
   3737
   3738    /* DEST (no-op) */
   3739    0x5: function (src, dst) {
   3740        // Do nothing
   3741    },
   3742
   3743    /* Invert SRC */
   3744    0xC: function (src, dst) {
   3745        dst.red   = 0xFF & ~src.red;
   3746        dst.green = 0xFF & ~src.green;
   3747        dst.blue  = 0xFF & ~src.blue;
   3748        dst.alpha =  src.alpha;
   3749    },
   3750    
   3751    /* Invert DEST */
   3752    0xA: function (src, dst) {
   3753        dst.red   = 0xFF & ~dst.red;
   3754        dst.green = 0xFF & ~dst.green;
   3755        dst.blue  = 0xFF & ~dst.blue;
   3756    },
   3757
   3758    /* AND */
   3759    0x1: function (src, dst) {
   3760        dst.red   =  ( src.red   &  dst.red);
   3761        dst.green =  ( src.green &  dst.green);
   3762        dst.blue  =  ( src.blue  &  dst.blue);
   3763    },
   3764
   3765    /* NAND */
   3766    0xE: function (src, dst) {
   3767        dst.red   = 0xFF & ~( src.red   &  dst.red);
   3768        dst.green = 0xFF & ~( src.green &  dst.green);
   3769        dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
   3770    },
   3771
   3772    /* OR */
   3773    0x7: function (src, dst) {
   3774        dst.red   =  ( src.red   |  dst.red);
   3775        dst.green =  ( src.green |  dst.green);
   3776        dst.blue  =  ( src.blue  |  dst.blue);
   3777    },
   3778
   3779    /* NOR */
   3780    0x8: function (src, dst) {
   3781        dst.red   = 0xFF & ~( src.red   |  dst.red);
   3782        dst.green = 0xFF & ~( src.green |  dst.green);
   3783        dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
   3784    },
   3785
   3786    /* XOR */
   3787    0x6: function (src, dst) {
   3788        dst.red   =  ( src.red   ^  dst.red);
   3789        dst.green =  ( src.green ^  dst.green);
   3790        dst.blue  =  ( src.blue  ^  dst.blue);
   3791    },
   3792
   3793    /* XNOR */
   3794    0x9: function (src, dst) {
   3795        dst.red   = 0xFF & ~( src.red   ^  dst.red);
   3796        dst.green = 0xFF & ~( src.green ^  dst.green);
   3797        dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
   3798    },
   3799
   3800    /* AND inverted source */
   3801    0x4: function (src, dst) {
   3802        dst.red   =  0xFF & (~src.red   &  dst.red);
   3803        dst.green =  0xFF & (~src.green &  dst.green);
   3804        dst.blue  =  0xFF & (~src.blue  &  dst.blue);
   3805    },
   3806
   3807    /* OR inverted source */
   3808    0xD: function (src, dst) {
   3809        dst.red   =  0xFF & (~src.red   |  dst.red);
   3810        dst.green =  0xFF & (~src.green |  dst.green);
   3811        dst.blue  =  0xFF & (~src.blue  |  dst.blue);
   3812    },
   3813
   3814    /* AND inverted destination */
   3815    0x2: function (src, dst) {
   3816        dst.red   =  0xFF & ( src.red   & ~dst.red);
   3817        dst.green =  0xFF & ( src.green & ~dst.green);
   3818        dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
   3819    },
   3820
   3821    /* OR inverted destination */
   3822    0xB: function (src, dst) {
   3823        dst.red   =  0xFF & ( src.red   | ~dst.red);
   3824        dst.green =  0xFF & ( src.green | ~dst.green);
   3825        dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
   3826    }
   3827
   3828};
   3829
   3830/**
   3831 * A list of possible messages that can be sent by the server for processing
   3832 * by the client.
   3833 * 
   3834 * @type {!Object.<string, number>}
   3835 */
   3836Guacamole.Client.Message = {
   3837    
   3838    /**
   3839     * A client message that indicates that a user has joined an existing
   3840     * connection. This message expects a single additional argument - the
   3841     * name of the user who has joined the connection.
   3842     * 
   3843     * @type {!number}
   3844     */
   3845    "USER_JOINED": 0x0001,
   3846    
   3847    /**
   3848     * A client message that indicates that a user has left an existing
   3849     * connection. This message expects a single additional argument - the
   3850     * name of the user who has left the connection.
   3851     * 
   3852     * @type {!number}
   3853     */
   3854    "USER_LEFT": 0x0002
   3855    
   3856};
   3857/*
   3858 * Licensed to the Apache Software Foundation (ASF) under one
   3859 * or more contributor license agreements.  See the NOTICE file
   3860 * distributed with this work for additional information
   3861 * regarding copyright ownership.  The ASF licenses this file
   3862 * to you under the Apache License, Version 2.0 (the
   3863 * "License"); you may not use this file except in compliance
   3864 * with the License.  You may obtain a copy of the License at
   3865 *
   3866 *   http://www.apache.org/licenses/LICENSE-2.0
   3867 *
   3868 * Unless required by applicable law or agreed to in writing,
   3869 * software distributed under the License is distributed on an
   3870 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   3871 * KIND, either express or implied.  See the License for the
   3872 * specific language governing permissions and limitations
   3873 * under the License.
   3874 */
   3875
   3876var Guacamole = Guacamole || {};
   3877
   3878/**
   3879 * A reader which automatically handles the given input stream, returning
   3880 * received blobs as a single data URI built over the course of the stream.
   3881 * Note that this object will overwrite any installed event handlers on the
   3882 * given Guacamole.InputStream.
   3883 * 
   3884 * @constructor
   3885 * @param {!Guacamole.InputStream} stream
   3886 *     The stream that data will be read from.
   3887 *
   3888 * @param {!string} mimetype
   3889 *     The mimetype of the data being received.
   3890 */
   3891Guacamole.DataURIReader = function(stream, mimetype) {
   3892
   3893    /**
   3894     * Reference to this Guacamole.DataURIReader.
   3895     *
   3896     * @private
   3897     * @type {!Guacamole.DataURIReader}
   3898     */
   3899    var guac_reader = this;
   3900
   3901    /**
   3902     * Current data URI.
   3903     *
   3904     * @private
   3905     * @type {!string}
   3906     */
   3907    var uri = 'data:' + mimetype + ';base64,';
   3908
   3909    // Receive blobs as array buffers
   3910    stream.onblob = function dataURIReaderBlob(data) {
   3911
   3912        // Currently assuming data will ALWAYS be safe to simply append. This
   3913        // will not be true if the received base64 data encodes a number of
   3914        // bytes that isn't a multiple of three (as base64 expands in a ratio
   3915        // of exactly 3:4).
   3916        uri += data;
   3917
   3918    };
   3919
   3920    // Simply call onend when end received
   3921    stream.onend = function dataURIReaderEnd() {
   3922        if (guac_reader.onend)
   3923            guac_reader.onend();
   3924    };
   3925
   3926    /**
   3927     * Returns the data URI of all data received through the underlying stream
   3928     * thus far.
   3929     *
   3930     * @returns {!string}
   3931     *     The data URI of all data received through the underlying stream thus
   3932     *     far.
   3933     */
   3934    this.getURI = function getURI() {
   3935        return uri;
   3936    };
   3937
   3938    /**
   3939     * Fired once this stream is finished and no further data will be written.
   3940     *
   3941     * @event
   3942     */
   3943    this.onend = null;
   3944
   3945};/*
   3946 * Licensed to the Apache Software Foundation (ASF) under one
   3947 * or more contributor license agreements.  See the NOTICE file
   3948 * distributed with this work for additional information
   3949 * regarding copyright ownership.  The ASF licenses this file
   3950 * to you under the Apache License, Version 2.0 (the
   3951 * "License"); you may not use this file except in compliance
   3952 * with the License.  You may obtain a copy of the License at
   3953 *
   3954 *   http://www.apache.org/licenses/LICENSE-2.0
   3955 *
   3956 * Unless required by applicable law or agreed to in writing,
   3957 * software distributed under the License is distributed on an
   3958 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   3959 * KIND, either express or implied.  See the License for the
   3960 * specific language governing permissions and limitations
   3961 * under the License.
   3962 */
   3963
   3964var Guacamole = Guacamole || {};
   3965
   3966/**
   3967 * The Guacamole display. The display does not deal with the Guacamole
   3968 * protocol, and instead implements a set of graphical operations which
   3969 * embody the set of operations present in the protocol. The order operations
   3970 * are executed is guaranteed to be in the same order as their corresponding
   3971 * functions are called.
   3972 * 
   3973 * @constructor
   3974 */
   3975Guacamole.Display = function() {
   3976
   3977    /**
   3978     * Reference to this Guacamole.Display.
   3979     * @private
   3980     */
   3981    var guac_display = this;
   3982
   3983    var displayWidth = 0;
   3984    var displayHeight = 0;
   3985    var displayScale = 1;
   3986
   3987    // Create display
   3988    var display = document.createElement("div");
   3989    display.style.position = "relative";
   3990    display.style.width = displayWidth + "px";
   3991    display.style.height = displayHeight + "px";
   3992
   3993    // Ensure transformations on display originate at 0,0
   3994    display.style.transformOrigin =
   3995    display.style.webkitTransformOrigin =
   3996    display.style.MozTransformOrigin =
   3997    display.style.OTransformOrigin =
   3998    display.style.msTransformOrigin =
   3999        "0 0";
   4000
   4001    // Create default layer
   4002    var default_layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);
   4003
   4004    // Create cursor layer
   4005    var cursor = new Guacamole.Display.VisibleLayer(0, 0);
   4006    cursor.setChannelMask(Guacamole.Layer.SRC);
   4007
   4008    // Add default layer and cursor to display
   4009    display.appendChild(default_layer.getElement());
   4010    display.appendChild(cursor.getElement());
   4011
   4012    // Create bounding div 
   4013    var bounds = document.createElement("div");
   4014    bounds.style.position = "relative";
   4015    bounds.style.width = (displayWidth*displayScale) + "px";
   4016    bounds.style.height = (displayHeight*displayScale) + "px";
   4017
   4018    // Add display to bounds
   4019    bounds.appendChild(display);
   4020
   4021    /**
   4022     * The X coordinate of the hotspot of the mouse cursor. The hotspot is
   4023     * the relative location within the image of the mouse cursor at which
   4024     * each click occurs.
   4025     * 
   4026     * @type {!number}
   4027     */
   4028    this.cursorHotspotX = 0;
   4029
   4030    /**
   4031     * The Y coordinate of the hotspot of the mouse cursor. The hotspot is
   4032     * the relative location within the image of the mouse cursor at which
   4033     * each click occurs.
   4034     * 
   4035     * @type {!number}
   4036     */
   4037    this.cursorHotspotY = 0;
   4038
   4039    /**
   4040     * The current X coordinate of the local mouse cursor. This is not
   4041     * necessarily the location of the actual mouse - it refers only to
   4042     * the location of the cursor image within the Guacamole display, as
   4043     * last set by moveCursor().
   4044     * 
   4045     * @type {!number}
   4046     */
   4047    this.cursorX = 0;
   4048
   4049    /**
   4050     * The current X coordinate of the local mouse cursor. This is not
   4051     * necessarily the location of the actual mouse - it refers only to
   4052     * the location of the cursor image within the Guacamole display, as
   4053     * last set by moveCursor().
   4054     * 
   4055     * @type {!number}
   4056     */
   4057    this.cursorY = 0;
   4058
   4059    /**
   4060     * The number of milliseconds over which display rendering statistics
   4061     * should be gathered, dispatching {@link #onstatistics} events as those
   4062     * statistics are available. If set to zero, no statistics will be
   4063     * gathered.
   4064     *
   4065     * @default 0
   4066     * @type {!number}
   4067     */
   4068    this.statisticWindow = 0;
   4069
   4070    /**
   4071     * Fired when the default layer (and thus the entire Guacamole display)
   4072     * is resized.
   4073     * 
   4074     * @event
   4075     * @param {!number} width
   4076     *     The new width of the Guacamole display.
   4077     *
   4078     * @param {!number} height
   4079     *     The new height of the Guacamole display.
   4080     */
   4081    this.onresize = null;
   4082
   4083    /**
   4084     * Fired whenever the local cursor image is changed. This can be used to
   4085     * implement special handling of the client-side cursor, or to override
   4086     * the default use of a software cursor layer.
   4087     * 
   4088     * @event
   4089     * @param {!HTMLCanvasElement} canvas
   4090     *     The cursor image.
   4091     *
   4092     * @param {!number} x
   4093     *     The X-coordinate of the cursor hotspot.
   4094     *
   4095     * @param {!number} y
   4096     *     The Y-coordinate of the cursor hotspot.
   4097     */
   4098    this.oncursor = null;
   4099
   4100    /**
   4101     * Fired whenever performance statistics are available for recently-
   4102     * rendered frames. This event will fire only if {@link #statisticWindow}
   4103     * is non-zero.
   4104     *
   4105     * @event
   4106     * @param {!Guacamole.Display.Statistics} stats
   4107     *     An object containing general rendering performance statistics for
   4108     *     the remote desktop, Guacamole server, and Guacamole client.
   4109     */
   4110    this.onstatistics = null;
   4111
   4112    /**
   4113     * The queue of all pending Tasks. Tasks will be run in order, with new
   4114     * tasks added at the end of the queue and old tasks removed from the
   4115     * front of the queue (FIFO). These tasks will eventually be grouped
   4116     * into a Frame.
   4117     *
   4118     * @private
   4119     * @type {!Task[]}
   4120     */
   4121    var tasks = [];
   4122
   4123    /**
   4124     * The queue of all frames. Each frame is a pairing of an array of tasks
   4125     * and a callback which must be called when the frame is rendered.
   4126     *
   4127     * @private
   4128     * @type {!Frame[]}
   4129     */
   4130    var frames = [];
   4131
   4132    /**
   4133     * The ID of the animation frame request returned by the last call to
   4134     * requestAnimationFrame(). This value will only be set if the browser
   4135     * supports requestAnimationFrame(), if a frame render is currently
   4136     * pending, and if the current browser tab is currently focused (likely to
   4137     * handle requests for animation frames). In all other cases, this will be
   4138     * null.
   4139     *
   4140     * @private
   4141     * @type {number}
   4142     */
   4143    var inProgressFrame = null;
   4144
   4145    /**
   4146     * Flushes all pending frames synchronously. This function will block until
   4147     * all pending frames have rendered. If a frame is currently blocked by an
   4148     * asynchronous operation like an image load, this function will return
   4149     * after reaching that operation and the flush operation will
   4150     * automamtically resume after that operation completes.
   4151     *
   4152     * @private
   4153     */
   4154    var syncFlush = function syncFlush() {
   4155
   4156        var localTimestamp = 0;
   4157        var remoteTimestamp = 0;
   4158
   4159        var renderedLogicalFrames = 0;
   4160        var rendered_frames = 0;
   4161
   4162        // Draw all pending frames, if ready
   4163        while (rendered_frames < frames.length) {
   4164
   4165            var frame = frames[rendered_frames];
   4166            if (!frame.isReady())
   4167                break;
   4168
   4169            frame.flush();
   4170
   4171            localTimestamp = frame.localTimestamp;
   4172            remoteTimestamp = frame.remoteTimestamp;
   4173            renderedLogicalFrames += frame.logicalFrames;
   4174            rendered_frames++;
   4175
   4176        } 
   4177
   4178        // Remove rendered frames from array
   4179        frames.splice(0, rendered_frames);
   4180
   4181        if (rendered_frames)
   4182            notifyFlushed(localTimestamp, remoteTimestamp, renderedLogicalFrames);
   4183
   4184    };
   4185
   4186    /**
   4187     * Flushes all pending frames asynchronously. This function returns
   4188     * immediately, relying on requestAnimationFrame() to dictate when each
   4189     * frame should be flushed.
   4190     *
   4191     * @private
   4192     */
   4193    var asyncFlush = function asyncFlush() {
   4194
   4195        var continueFlush = function continueFlush() {
   4196
   4197            // We're no longer waiting to render a frame
   4198            inProgressFrame = null;
   4199
   4200            // Nothing to do if there are no frames remaining
   4201            if (!frames.length)
   4202                return;
   4203
   4204            // Flush the next frame only if it is ready (not awaiting
   4205            // completion of some asynchronous operation like an image load)
   4206            if (frames[0].isReady()) {
   4207                var frame = frames.shift();
   4208                frame.flush();
   4209                notifyFlushed(frame.localTimestamp, frame.remoteTimestamp, frame.logicalFrames);
   4210            }
   4211
   4212            // Request yet another animation frame if frames remain to be
   4213            // flushed
   4214            if (frames.length)
   4215                inProgressFrame = window.requestAnimationFrame(continueFlush);
   4216
   4217        };
   4218
   4219        // Begin flushing frames if not already waiting to render a frame
   4220        if (!inProgressFrame)
   4221            inProgressFrame = window.requestAnimationFrame(continueFlush);
   4222
   4223    };
   4224
   4225    /**
   4226     * Recently-gathered display render statistics, as made available by calls
   4227     * to notifyFlushed(). The contents of this array will be trimmed to
   4228     * contain only up to {@link #statisticWindow} milliseconds of statistics.
   4229     *
   4230     * @private
   4231     * @type {Guacamole.Display.Statistics[]}
   4232     */
   4233    var statistics = [];
   4234
   4235    /**
   4236     * Notifies that one or more frames have been successfully rendered
   4237     * (flushed) to the display.
   4238     *
   4239     * @private
   4240     * @param {!number} localTimestamp
   4241     *     The local timestamp of the point in time at which the most recent,
   4242     *     flushed frame was received by the display, in milliseconds since the
   4243     *     Unix Epoch.
   4244     *
   4245     * @param {!number} remoteTimestamp
   4246     *     The remote timestamp of sync instruction associated with the most
   4247     *     recent, flushed frame received by the display. This timestamp is in
   4248     *     milliseconds, but is arbitrary, having meaning only relative to
   4249     *     other timestamps in the same connection.
   4250     *
   4251     * @param {!number} logicalFrames
   4252     *     The number of remote desktop frames that were flushed.
   4253     */
   4254    var notifyFlushed = function notifyFlushed(localTimestamp, remoteTimestamp, logicalFrames) {
   4255
   4256        // Ignore if statistics are not being gathered
   4257        if (!guac_display.statisticWindow)
   4258            return;
   4259
   4260        var current = new Date().getTime();
   4261
   4262        // Find the first statistic that is still within the configured time
   4263        // window
   4264        for (var first = 0; first < statistics.length; first++) {
   4265            if (current - statistics[first].timestamp <= guac_display.statisticWindow)
   4266                break;
   4267        }
   4268
   4269        // Remove all statistics except those within the time window
   4270        statistics.splice(0, first - 1);
   4271
   4272        // Record statistics for latest frame
   4273        statistics.push({
   4274            localTimestamp : localTimestamp,
   4275            remoteTimestamp : remoteTimestamp,
   4276            timestamp : current,
   4277            frames : logicalFrames
   4278        });
   4279
   4280        // Determine the actual time interval of the available statistics (this
   4281        // will not perfectly match the configured interval, which is an upper
   4282        // bound)
   4283        var statDuration = (statistics[statistics.length - 1].timestamp - statistics[0].timestamp) / 1000;
   4284
   4285        // Determine the amount of time that elapsed remotely (within the
   4286        // remote desktop)
   4287        var remoteDuration = (statistics[statistics.length - 1].remoteTimestamp - statistics[0].remoteTimestamp) / 1000;
   4288
   4289        // Calculate the number of frames that have been rendered locally
   4290        // within the configured time interval
   4291        var localFrames = statistics.length;
   4292
   4293        // Calculate the number of frames actually received from the remote
   4294        // desktop by the Guacamole server
   4295        var remoteFrames = statistics.reduce(function sumFrames(prev, stat) {
   4296            return prev + stat.frames;
   4297        }, 0);
   4298
   4299        // Calculate the number of frames that the Guacamole server had to
   4300        // drop or combine with other frames
   4301        var drops = statistics.reduce(function sumDrops(prev, stat) {
   4302            return prev + Math.max(0, stat.frames - 1);
   4303        }, 0);
   4304
   4305        // Produce lag and FPS statistics from above raw measurements
   4306        var stats = new Guacamole.Display.Statistics({
   4307            processingLag : current - localTimestamp,
   4308            desktopFps : (remoteDuration && remoteFrames) ? remoteFrames / remoteDuration : null,
   4309            clientFps : statDuration ? localFrames / statDuration : null,
   4310            serverFps : remoteDuration ? localFrames / remoteDuration : null,
   4311            dropRate : remoteDuration ? drops / remoteDuration : null
   4312        });
   4313
   4314        // Notify of availability of new statistics
   4315        if (guac_display.onstatistics)
   4316            guac_display.onstatistics(stats);
   4317
   4318    };
   4319
   4320    // Switch from asynchronous frame handling to synchronous frame handling if
   4321    // requestAnimationFrame() is unlikely to be usable (browsers may not
   4322    // invoke the animation frame callback if the relevant tab is not focused)
   4323    window.addEventListener('blur', function switchToSyncFlush() {
   4324        if (inProgressFrame && !document.hasFocus()) {
   4325
   4326            // Cancel pending asynchronous processing of frame ...
   4327            window.cancelAnimationFrame(inProgressFrame);
   4328            inProgressFrame = null;
   4329
   4330            // ... and instead process it synchronously
   4331            syncFlush();
   4332
   4333        }
   4334    }, true);
   4335
   4336    /**
   4337     * Flushes all pending frames.
   4338     * @private
   4339     */
   4340    function __flush_frames() {
   4341
   4342        if (window.requestAnimationFrame && document.hasFocus())
   4343            asyncFlush();
   4344        else
   4345            syncFlush();
   4346
   4347    }
   4348
   4349    /**
   4350     * An ordered list of tasks which must be executed atomically. Once
   4351     * executed, an associated (and optional) callback will be called.
   4352     *
   4353     * @private
   4354     * @constructor
   4355     * @param {function} [callback]
   4356     *     The function to call when this frame is rendered.
   4357     *
   4358     * @param {!Task[]} tasks
   4359     *     The set of tasks which must be executed to render this frame.
   4360     *
   4361     * @param {number} [timestamp]
   4362     *     The remote timestamp of sync instruction associated with this frame.
   4363     *     This timestamp is in milliseconds, but is arbitrary, having meaning
   4364     *     only relative to other remote timestamps in the same connection. If
   4365     *     omitted, a compatible but local timestamp will be used instead.
   4366     *
   4367     * @param {number} [logicalFrames=0]
   4368     *     The number of remote desktop frames that were combined to produce
   4369     *     this frame, or zero if this value is unknown or inapplicable.
   4370     */
   4371    var Frame = function Frame(callback, tasks, timestamp, logicalFrames) {
   4372
   4373        /**
   4374         * The local timestamp of the point in time at which this frame was
   4375         * received by the display, in milliseconds since the Unix Epoch.
   4376         *
   4377         * @type {!number}
   4378         */
   4379        this.localTimestamp = new Date().getTime();
   4380
   4381        /**
   4382         * The remote timestamp of sync instruction associated with this frame.
   4383         * This timestamp is in milliseconds, but is arbitrary, having meaning
   4384         * only relative to other remote timestamps in the same connection.
   4385         *
   4386         * @type {!number}
   4387         */
   4388        this.remoteTimestamp = timestamp || this.localTimestamp;
   4389
   4390        /**
   4391         * The number of remote desktop frames that were combined to produce
   4392         * this frame. If unknown or not applicable, this will be zero.
   4393         *
   4394         * @type {!number}
   4395         */
   4396        this.logicalFrames = logicalFrames || 0;
   4397
   4398        /**
   4399         * Cancels rendering of this frame and all associated tasks. The
   4400         * callback provided at construction time, if any, is not invoked.
   4401         */
   4402        this.cancel = function cancel() {
   4403
   4404            callback = null;
   4405
   4406            tasks.forEach(function cancelTask(task) {
   4407                task.cancel();
   4408            });
   4409
   4410            tasks = [];
   4411
   4412        };
   4413
   4414        /**
   4415         * Returns whether this frame is ready to be rendered. This function
   4416         * returns true if and only if ALL underlying tasks are unblocked.
   4417         * 
   4418         * @returns {!boolean}
   4419         *     true if all underlying tasks are unblocked, false otherwise.
   4420         */
   4421        this.isReady = function() {
   4422
   4423            // Search for blocked tasks
   4424            for (var i=0; i < tasks.length; i++) {
   4425                if (tasks[i].blocked)
   4426                    return false;
   4427            }
   4428
   4429            // If no blocked tasks, the frame is ready
   4430            return true;
   4431
   4432        };
   4433
   4434        /**
   4435         * Renders this frame, calling the associated callback, if any, after
   4436         * the frame is complete. This function MUST only be called when no
   4437         * blocked tasks exist. Calling this function with blocked tasks
   4438         * will result in undefined behavior.
   4439         */
   4440        this.flush = function() {
   4441
   4442            // Draw all pending tasks.
   4443            for (var i=0; i < tasks.length; i++)
   4444                tasks[i].execute();
   4445
   4446            // Call callback
   4447            if (callback) callback();
   4448
   4449        };
   4450
   4451    };
   4452
   4453    /**
   4454     * A container for an task handler. Each operation which must be ordered
   4455     * is associated with a Task that goes into a task queue. Tasks in this
   4456     * queue are executed in order once their handlers are set, while Tasks 
   4457     * without handlers block themselves and any following Tasks from running.
   4458     *
   4459     * @constructor
   4460     * @private
   4461     * @param {function} [taskHandler]
   4462     *     The function to call when this task runs, if any.
   4463     *
   4464     * @param {boolean} [blocked]
   4465     *     Whether this task should start blocked.
   4466     */
   4467    function Task(taskHandler, blocked) {
   4468
   4469        /**
   4470         * Reference to this Task.
   4471         *
   4472         * @private
   4473         * @type {!Guacamole.Display.Task}
   4474         */
   4475        var task = this;
   4476       
   4477        /**
   4478         * Whether this Task is blocked.
   4479         * 
   4480         * @type {boolean}
   4481         */
   4482        this.blocked = blocked;
   4483
   4484        /**
   4485         * Cancels this task such that it will not run. The task handler
   4486         * provided at construction time, if any, is not invoked. Calling
   4487         * execute() after calling this function has no effect.
   4488         */
   4489        this.cancel = function cancel() {
   4490            task.blocked = false;
   4491            taskHandler = null;
   4492        };
   4493
   4494        /**
   4495         * Unblocks this Task, allowing it to run.
   4496         */
   4497        this.unblock = function() {
   4498            if (task.blocked) {
   4499                task.blocked = false;
   4500                __flush_frames();
   4501            }
   4502        };
   4503
   4504        /**
   4505         * Calls the handler associated with this task IMMEDIATELY. This
   4506         * function does not track whether this task is marked as blocked.
   4507         * Enforcing the blocked status of tasks is up to the caller.
   4508         */
   4509        this.execute = function() {
   4510            if (taskHandler) taskHandler();
   4511        };
   4512
   4513    }
   4514
   4515    /**
   4516     * Schedules a task for future execution. The given handler will execute
   4517     * immediately after all previous tasks upon frame flush, unless this
   4518     * task is blocked. If any tasks is blocked, the entire frame will not
   4519     * render (and no tasks within will execute) until all tasks are unblocked.
   4520     * 
   4521     * @private
   4522     * @param {function} [handler]
   4523     *     The function to call when possible, if any.
   4524     *
   4525     * @param {boolean} [blocked]
   4526     *     Whether the task should start blocked.
   4527     *
   4528     * @returns {!Task}
   4529     *     The Task created and added to the queue for future running.
   4530     */
   4531    function scheduleTask(handler, blocked) {
   4532        var task = new Task(handler, blocked);
   4533        tasks.push(task);
   4534        return task;
   4535    }
   4536
   4537    /**
   4538     * Returns the element which contains the Guacamole display.
   4539     * 
   4540     * @return {!Element}
   4541     *     The element containing the Guacamole display.
   4542     */
   4543    this.getElement = function() {
   4544        return bounds;
   4545    };
   4546
   4547    /**
   4548     * Returns the width of this display.
   4549     * 
   4550     * @return {!number}
   4551     *     The width of this display;
   4552     */
   4553    this.getWidth = function() {
   4554        return displayWidth;
   4555    };
   4556
   4557    /**
   4558     * Returns the height of this display.
   4559     * 
   4560     * @return {!number}
   4561     *     The height of this display;
   4562     */
   4563    this.getHeight = function() {
   4564        return displayHeight;
   4565    };
   4566
   4567    /**
   4568     * Returns the default layer of this display. Each Guacamole display always
   4569     * has at least one layer. Other layers can optionally be created within
   4570     * this layer, but the default layer cannot be removed and is the absolute
   4571     * ancestor of all other layers.
   4572     * 
   4573     * @return {!Guacamole.Display.VisibleLayer}
   4574     *     The default layer.
   4575     */
   4576    this.getDefaultLayer = function() {
   4577        return default_layer;
   4578    };
   4579
   4580    /**
   4581     * Returns the cursor layer of this display. Each Guacamole display contains
   4582     * a layer for the image of the mouse cursor. This layer is a special case
   4583     * and exists above all other layers, similar to the hardware mouse cursor.
   4584     * 
   4585     * @return {!Guacamole.Display.VisibleLayer}
   4586     *     The cursor layer.
   4587     */
   4588    this.getCursorLayer = function() {
   4589        return cursor;
   4590    };
   4591
   4592    /**
   4593     * Creates a new layer. The new layer will be a direct child of the default
   4594     * layer, but can be moved to be a child of any other layer. Layers returned
   4595     * by this function are visible.
   4596     * 
   4597     * @return {!Guacamole.Display.VisibleLayer}
   4598     *     The newly-created layer.
   4599     */
   4600    this.createLayer = function() {
   4601        var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight);
   4602        layer.move(default_layer, 0, 0, 0);
   4603        return layer;
   4604    };
   4605
   4606    /**
   4607     * Creates a new buffer. Buffers are invisible, off-screen surfaces. They
   4608     * are implemented in the same manner as layers, but do not provide the
   4609     * same nesting semantics.
   4610     * 
   4611     * @return {!Guacamole.Layer}
   4612     *     The newly-created buffer.
   4613     */
   4614    this.createBuffer = function() {
   4615        var buffer = new Guacamole.Layer(0, 0);
   4616        buffer.autosize = 1;
   4617        return buffer;
   4618    };
   4619
   4620    /**
   4621     * Flush all pending draw tasks, if possible, as a new frame. If the entire
   4622     * frame is not ready, the flush will wait until all required tasks are
   4623     * unblocked.
   4624     * 
   4625     * @param {function} [callback]
   4626     *     The function to call when this frame is flushed. This may happen
   4627     *     immediately, or later when blocked tasks become unblocked.
   4628     *
   4629     * @param {number} timestamp
   4630     *     The remote timestamp of sync instruction associated with this frame.
   4631     *     This timestamp is in milliseconds, but is arbitrary, having meaning
   4632     *     only relative to other remote timestamps in the same connection.
   4633     *
   4634     * @param {number} logicalFrames
   4635     *     The number of remote desktop frames that were combined to produce
   4636     *     this frame.
   4637     */
   4638    this.flush = function(callback, timestamp, logicalFrames) {
   4639
   4640        // Add frame, reset tasks
   4641        frames.push(new Frame(callback, tasks, timestamp, logicalFrames));
   4642        tasks = [];
   4643
   4644        // Attempt flush
   4645        __flush_frames();
   4646
   4647    };
   4648
   4649    /**
   4650     * Cancels rendering of all pending frames and associated rendering
   4651     * operations. The callbacks provided to outstanding past calls to flush(),
   4652     * if any, are not invoked.
   4653     */
   4654    this.cancel = function cancel() {
   4655
   4656        frames.forEach(function cancelFrame(frame) {
   4657            frame.cancel();
   4658        });
   4659
   4660        frames = [];
   4661
   4662        tasks.forEach(function cancelTask(task) {
   4663            task.cancel();
   4664        });
   4665
   4666        tasks = [];
   4667
   4668    };
   4669
   4670    /**
   4671     * Sets the hotspot and image of the mouse cursor displayed within the
   4672     * Guacamole display.
   4673     * 
   4674     * @param {!number} hotspotX
   4675     *     The X coordinate of the cursor hotspot.
   4676     *
   4677     * @param {!number} hotspotY
   4678     *     The Y coordinate of the cursor hotspot.
   4679     *
   4680     * @param {!Guacamole.Layer} layer
   4681     *     The source layer containing the data which should be used as the
   4682     *     mouse cursor image.
   4683     *
   4684     * @param {!number} srcx
   4685     *     The X coordinate of the upper-left corner of the rectangle within
   4686     *     the source layer's coordinate space to copy data from.
   4687     *
   4688     * @param {!number} srcy
   4689     *     The Y coordinate of the upper-left corner of the rectangle within
   4690     *     the source layer's coordinate space to copy data from.
   4691     *
   4692     * @param {!number} srcw
   4693     *     The width of the rectangle within the source layer's coordinate
   4694     *     space to copy data from.
   4695     *
   4696     * @param {!number} srch
   4697     *     The height of the rectangle within the source layer's coordinate
   4698     *     space to copy data from.
   4699     */
   4700    this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) {
   4701        scheduleTask(function __display_set_cursor() {
   4702
   4703            // Set hotspot
   4704            guac_display.cursorHotspotX = hotspotX;
   4705            guac_display.cursorHotspotY = hotspotY;
   4706
   4707            // Reset cursor size
   4708            cursor.resize(srcw, srch);
   4709
   4710            // Draw cursor to cursor layer
   4711            cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0);
   4712            guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY);
   4713
   4714            // Fire cursor change event
   4715            if (guac_display.oncursor)
   4716                guac_display.oncursor(cursor.toCanvas(), hotspotX, hotspotY);
   4717
   4718        });
   4719    };
   4720
   4721    /**
   4722     * Sets whether the software-rendered cursor is shown. This cursor differs
   4723     * from the hardware cursor in that it is built into the Guacamole.Display,
   4724     * and relies on its own Guacamole layer to render.
   4725     *
   4726     * @param {boolean} [shown=true]
   4727     *     Whether to show the software cursor.
   4728     */
   4729    this.showCursor = function(shown) {
   4730
   4731        var element = cursor.getElement();
   4732        var parent = element.parentNode;
   4733
   4734        // Remove from DOM if hidden
   4735        if (shown === false) {
   4736            if (parent)
   4737                parent.removeChild(element);
   4738        }
   4739
   4740        // Otherwise, ensure cursor is child of display
   4741        else if (parent !== display)
   4742            display.appendChild(element);
   4743
   4744    };
   4745
   4746    /**
   4747     * Sets the location of the local cursor to the given coordinates. For the
   4748     * sake of responsiveness, this function performs its action immediately.
   4749     * Cursor motion is not maintained within atomic frames.
   4750     * 
   4751     * @param {!number} x
   4752     *     The X coordinate to move the cursor to.
   4753     *
   4754     * @param {!number} y
   4755     *     The Y coordinate to move the cursor to.
   4756     */
   4757    this.moveCursor = function(x, y) {
   4758
   4759        // Move cursor layer
   4760        cursor.translate(x - guac_display.cursorHotspotX,
   4761                         y - guac_display.cursorHotspotY);
   4762
   4763        // Update stored position
   4764        guac_display.cursorX = x;
   4765        guac_display.cursorY = y;
   4766
   4767    };
   4768
   4769    /**
   4770     * Changes the size of the given Layer to the given width and height.
   4771     * Resizing is only attempted if the new size provided is actually different
   4772     * from the current size.
   4773     * 
   4774     * @param {!Guacamole.Layer} layer
   4775     *     The layer to resize.
   4776     *
   4777     * @param {!number} width
   4778     *     The new width.
   4779     *
   4780     * @param {!number} height
   4781     *     The new height.
   4782     */
   4783    this.resize = function(layer, width, height) {
   4784        scheduleTask(function __display_resize() {
   4785
   4786            layer.resize(width, height);
   4787
   4788            // Resize display if default layer is resized
   4789            if (layer === default_layer) {
   4790
   4791                // Update (set) display size
   4792                displayWidth = width;
   4793                displayHeight = height;
   4794                display.style.width = displayWidth + "px";
   4795                display.style.height = displayHeight + "px";
   4796
   4797                // Update bounds size
   4798                bounds.style.width = (displayWidth*displayScale) + "px";
   4799                bounds.style.height = (displayHeight*displayScale) + "px";
   4800
   4801                // Notify of resize
   4802                if (guac_display.onresize)
   4803                    guac_display.onresize(width, height);
   4804
   4805            }
   4806
   4807        });
   4808    };
   4809
   4810    /**
   4811     * Draws the specified image at the given coordinates. The image specified
   4812     * must already be loaded.
   4813     * 
   4814     * @param {!Guacamole.Layer} layer
   4815     *     The layer to draw upon.
   4816     *
   4817     * @param {!number} x
   4818     *     The destination X coordinate.
   4819     *
   4820     * @param {!number} y 
   4821     *     The destination Y coordinate.
   4822     *
   4823     * @param {!CanvasImageSource} image
   4824     *     The image to draw. Note that this not a URL.
   4825     */
   4826    this.drawImage = function(layer, x, y, image) {
   4827        scheduleTask(function __display_drawImage() {
   4828            layer.drawImage(x, y, image);
   4829        });
   4830    };
   4831
   4832    /**
   4833     * Draws the image contained within the specified Blob at the given
   4834     * coordinates. The Blob specified must already be populated with image
   4835     * data.
   4836     *
   4837     * @param {!Guacamole.Layer} layer
   4838     *     The layer to draw upon.
   4839     *
   4840     * @param {!number} x
   4841     *     The destination X coordinate.
   4842     *
   4843     * @param {!number} y
   4844     *     The destination Y coordinate.
   4845     *
   4846     * @param {!Blob} blob
   4847     *     The Blob containing the image data to draw.
   4848     */
   4849    this.drawBlob = function(layer, x, y, blob) {
   4850
   4851        var task;
   4852
   4853        // Prefer createImageBitmap() over blob URLs if available
   4854        if (window.createImageBitmap) {
   4855
   4856            var bitmap;
   4857
   4858            // Draw image once loaded
   4859            task = scheduleTask(function drawImageBitmap() {
   4860                layer.drawImage(x, y, bitmap);
   4861            }, true);
   4862
   4863            // Load image from provided blob
   4864            window.createImageBitmap(blob).then(function bitmapLoaded(decoded) {
   4865                bitmap = decoded;
   4866                task.unblock();
   4867            });
   4868
   4869        }
   4870
   4871        // Use blob URLs and the Image object if createImageBitmap() is
   4872        // unavailable
   4873        else {
   4874
   4875            // Create URL for blob
   4876            var url = URL.createObjectURL(blob);
   4877
   4878            // Draw and free blob URL when ready
   4879            task = scheduleTask(function __display_drawBlob() {
   4880
   4881                // Draw the image only if it loaded without errors
   4882                if (image.width && image.height)
   4883                    layer.drawImage(x, y, image);
   4884
   4885                // Blob URL no longer needed
   4886                URL.revokeObjectURL(url);
   4887
   4888            }, true);
   4889
   4890            // Load image from URL
   4891            var image = new Image();
   4892            image.onload = task.unblock;
   4893            image.onerror = task.unblock;
   4894            image.src = url;
   4895
   4896        }
   4897
   4898    };
   4899
   4900    /**
   4901     * Draws the image within the given stream at the given coordinates. The
   4902     * image will be loaded automatically, and this and any future operations
   4903     * will wait for the image to finish loading. This function will
   4904     * automatically choose an appropriate method for reading and decoding the
   4905     * given image stream, and should be preferred for received streams except
   4906     * where manual decoding of the stream is unavoidable.
   4907     *
   4908     * @param {!Guacamole.Layer} layer
   4909     *     The layer to draw upon.
   4910     *
   4911     * @param {!number} x
   4912     *     The destination X coordinate.
   4913     *
   4914     * @param {!number} y
   4915     *     The destination Y coordinate.
   4916     *
   4917     * @param {!Guacamole.InputStream} stream
   4918     *     The stream along which image data will be received.
   4919     *
   4920     * @param {!string} mimetype
   4921     *     The mimetype of the image within the stream.
   4922     */
   4923    this.drawStream = function drawStream(layer, x, y, stream, mimetype) {
   4924
   4925        // If createImageBitmap() is available, load the image as a blob so
   4926        // that function can be used
   4927        if (window.createImageBitmap) {
   4928            var reader = new Guacamole.BlobReader(stream, mimetype);
   4929            reader.onend = function drawImageBlob() {
   4930                guac_display.drawBlob(layer, x, y, reader.getBlob());
   4931            };
   4932        }
   4933
   4934        // Lacking createImageBitmap(), fall back to data URIs and the Image
   4935        // object
   4936        else {
   4937            var reader = new Guacamole.DataURIReader(stream, mimetype);
   4938            reader.onend = function drawImageDataURI() {
   4939                guac_display.draw(layer, x, y, reader.getURI());
   4940            };
   4941        }
   4942
   4943    };
   4944
   4945    /**
   4946     * Draws the image at the specified URL at the given coordinates. The image
   4947     * will be loaded automatically, and this and any future operations will
   4948     * wait for the image to finish loading.
   4949     * 
   4950     * @param {!Guacamole.Layer} layer
   4951     *     The layer to draw upon.
   4952     *
   4953     * @param {!number} x
   4954     *     The destination X coordinate.
   4955     *
   4956     * @param {!number} y
   4957     *     The destination Y coordinate.
   4958     *
   4959     * @param {!string} url
   4960     *     The URL of the image to draw.
   4961     */
   4962    this.draw = function(layer, x, y, url) {
   4963
   4964        var task = scheduleTask(function __display_draw() {
   4965
   4966            // Draw the image only if it loaded without errors
   4967            if (image.width && image.height)
   4968                layer.drawImage(x, y, image);
   4969
   4970        }, true);
   4971
   4972        var image = new Image();
   4973        image.onload = task.unblock;
   4974        image.onerror = task.unblock;
   4975        image.src = url;
   4976
   4977    };
   4978
   4979    /**
   4980     * Plays the video at the specified URL within this layer. The video
   4981     * will be loaded automatically, and this and any future operations will
   4982     * wait for the video to finish loading. Future operations will not be
   4983     * executed until the video finishes playing.
   4984     * 
   4985     * @param {!Guacamole.Layer} layer
   4986     *     The layer to draw upon.
   4987     *
   4988     * @param {!string} mimetype
   4989     *     The mimetype of the video to play.
   4990     *
   4991     * @param {!number} duration
   4992     *     The duration of the video in milliseconds.
   4993     *
   4994     * @param {!string} url
   4995     *     The URL of the video to play.
   4996     */
   4997    this.play = function(layer, mimetype, duration, url) {
   4998
   4999        // Start loading the video
   5000        var video = document.createElement("video");
   5001        video.type = mimetype;
   5002        video.src = url;
   5003
   5004        // Start copying frames when playing
   5005        video.addEventListener("play", function() {
   5006            
   5007            function render_callback() {
   5008                layer.drawImage(0, 0, video);
   5009                if (!video.ended)
   5010                    window.setTimeout(render_callback, 20);
   5011            }
   5012            
   5013            render_callback();
   5014            
   5015        }, false);
   5016
   5017        scheduleTask(video.play);
   5018
   5019    };
   5020
   5021    /**
   5022     * Transfer a rectangle of image data from one Layer to this Layer using the
   5023     * specified transfer function.
   5024     * 
   5025     * @param {!Guacamole.Layer} srcLayer
   5026     *     The Layer to copy image data from.
   5027     *
   5028     * @param {!number} srcx
   5029     *     The X coordinate of the upper-left corner of the rectangle within
   5030     *     the source Layer's coordinate space to copy data from.
   5031     *
   5032     * @param {!number} srcy
   5033     *     The Y coordinate of the upper-left corner of the rectangle within
   5034     *     the source Layer's coordinate space to copy data from.
   5035     *
   5036     * @param {!number} srcw
   5037     *     The width of the rectangle within the source Layer's coordinate
   5038     *     space to copy data from.
   5039     *
   5040     * @param {!number} srch
   5041     *     The height of the rectangle within the source Layer's coordinate
   5042     *     space to copy data from.
   5043     *
   5044     * @param {!Guacamole.Layer} dstLayer
   5045     *     The layer to draw upon.
   5046     *
   5047     * @param {!number} x
   5048     *     The destination X coordinate.
   5049     *
   5050     * @param {!number} y
   5051     *     The destination Y coordinate.
   5052     *
   5053     * @param {!function} transferFunction
   5054     *     The transfer function to use to transfer data from source to
   5055     *     destination.
   5056     */
   5057    this.transfer = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y, transferFunction) {
   5058        scheduleTask(function __display_transfer() {
   5059            dstLayer.transfer(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction);
   5060        });
   5061    };
   5062
   5063    /**
   5064     * Put a rectangle of image data from one Layer to this Layer directly
   5065     * without performing any alpha blending. Simply copy the data.
   5066     * 
   5067     * @param {!Guacamole.Layer} srcLayer
   5068     *     The Layer to copy image data from.
   5069     *
   5070     * @param {!number} srcx
   5071     *     The X coordinate of the upper-left corner of the rectangle within
   5072     *     the source Layer's coordinate space to copy data from.
   5073     *
   5074     * @param {!number} srcy
   5075     *     The Y coordinate of the upper-left corner of the rectangle within
   5076     *     the source Layer's coordinate space to copy data from.
   5077     *
   5078     * @param {!number} srcw
   5079     *     The width of the rectangle within the source Layer's coordinate
   5080     *     space to copy data from.
   5081     *
   5082     * @param {!number} srch
   5083     *     The height of the rectangle within the source Layer's coordinate
   5084     *     space to copy data from.
   5085     *
   5086     * @param {!Guacamole.Layer} dstLayer
   5087     *     The layer to draw upon.
   5088     *
   5089     * @param {!number} x
   5090     *     The destination X coordinate.
   5091     *
   5092     * @param {!number} y
   5093     *     The destination Y coordinate.
   5094     */
   5095    this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
   5096        scheduleTask(function __display_put() {
   5097            dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y);
   5098        });
   5099    };
   5100
   5101    /**
   5102     * Copy a rectangle of image data from one Layer to this Layer. This
   5103     * operation will copy exactly the image data that will be drawn once all
   5104     * operations of the source Layer that were pending at the time this
   5105     * function was called are complete. This operation will not alter the
   5106     * size of the source Layer even if its autosize property is set to true.
   5107     * 
   5108     * @param {!Guacamole.Layer} srcLayer
   5109     *     The Layer to copy image data from.
   5110     *
   5111     * @param {!number} srcx
   5112     *     The X coordinate of the upper-left corner of the rectangle within
   5113     *     the source Layer's coordinate space to copy data from.
   5114     *
   5115     * @param {!number} srcy
   5116     *     The Y coordinate of the upper-left corner of the rectangle within
   5117     *     the source Layer's coordinate space to copy data from.
   5118     *
   5119     * @param {!number} srcw
   5120     *     The width of the rectangle within the source Layer's coordinate
   5121     *     space to copy data from.
   5122     *
   5123     * @param {!number} srch
   5124     *     The height of the rectangle within the source Layer's coordinate space to copy data from.
   5125     *
   5126     * @param {!Guacamole.Layer} dstLayer
   5127     *     The layer to draw upon.
   5128     *
   5129     * @param {!number} x
   5130     *     The destination X coordinate.
   5131     *
   5132     * @param {!number} y
   5133     *     The destination Y coordinate.
   5134     */
   5135    this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) {
   5136        scheduleTask(function __display_copy() {
   5137            dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y);
   5138        });
   5139    };
   5140
   5141    /**
   5142     * Starts a new path at the specified point.
   5143     * 
   5144     * @param {!Guacamole.Layer} layer
   5145     *     The layer to draw upon.
   5146     *
   5147     * @param {!number} x
   5148     *     The X coordinate of the point to draw.
   5149     *
   5150     * @param {!number} y
   5151     *     The Y coordinate of the point to draw.
   5152     */
   5153    this.moveTo = function(layer, x, y) {
   5154        scheduleTask(function __display_moveTo() {
   5155            layer.moveTo(x, y);
   5156        });
   5157    };
   5158
   5159    /**
   5160     * Add the specified line to the current path.
   5161     * 
   5162     * @param {!Guacamole.Layer} layer
   5163     *     The layer to draw upon.
   5164     *
   5165     * @param {!number} x
   5166     *     The X coordinate of the endpoint of the line to draw.
   5167     *
   5168     * @param {!number} y
   5169     *     The Y coordinate of the endpoint of the line to draw.
   5170     */
   5171    this.lineTo = function(layer, x, y) {
   5172        scheduleTask(function __display_lineTo() {
   5173            layer.lineTo(x, y);
   5174        });
   5175    };
   5176
   5177    /**
   5178     * Add the specified arc to the current path.
   5179     *
   5180     * @param {!Guacamole.Layer} layer
   5181     *     The layer to draw upon.
   5182     *
   5183     * @param {!number} x
   5184     *     The X coordinate of the center of the circle which will contain the
   5185     *     arc.
   5186     *
   5187     * @param {!number} y
   5188     *     The Y coordinate of the center of the circle which will contain the
   5189     *     arc.
   5190     *
   5191     * @param {!number} radius
   5192     *     The radius of the circle.
   5193     *
   5194     * @param {!number} startAngle
   5195     *     The starting angle of the arc, in radians.
   5196     *
   5197     * @param {!number} endAngle
   5198     *     The ending angle of the arc, in radians.
   5199     *
   5200     * @param {!boolean} negative
   5201     *     Whether the arc should be drawn in order of decreasing angle.
   5202     */
   5203    this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) {
   5204        scheduleTask(function __display_arc() {
   5205            layer.arc(x, y, radius, startAngle, endAngle, negative);
   5206        });
   5207    };
   5208
   5209    /**
   5210     * Starts a new path at the specified point.
   5211     *
   5212     * @param {!Guacamole.Layer} layer
   5213     *     The layer to draw upon.
   5214     *
   5215     * @param {!number} cp1x
   5216     *     The X coordinate of the first control point.
   5217     *
   5218     * @param {!number} cp1y
   5219     *     The Y coordinate of the first control point.
   5220     *
   5221     * @param {!number} cp2x
   5222     *     The X coordinate of the second control point.
   5223     *
   5224     * @param {!number} cp2y
   5225     *     The Y coordinate of the second control point.
   5226     *
   5227     * @param {!number} x
   5228     *     The X coordinate of the endpoint of the curve.
   5229     *
   5230     * @param {!number} y
   5231     *     The Y coordinate of the endpoint of the curve.
   5232     */
   5233    this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) {
   5234        scheduleTask(function __display_curveTo() {
   5235            layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
   5236        });
   5237    };
   5238
   5239    /**
   5240     * Closes the current path by connecting the end point with the start
   5241     * point (if any) with a straight line.
   5242     * 
   5243     * @param {!Guacamole.Layer} layer
   5244     *     The layer to draw upon.
   5245     */
   5246    this.close = function(layer) {
   5247        scheduleTask(function __display_close() {
   5248            layer.close();
   5249        });
   5250    };
   5251
   5252    /**
   5253     * Add the specified rectangle to the current path.
   5254     *
   5255     * @param {!Guacamole.Layer} layer
   5256     *     The layer to draw upon.
   5257     *
   5258     * @param {!number} x
   5259     *     The X coordinate of the upper-left corner of the rectangle to draw.
   5260     *
   5261     * @param {!number} y
   5262     *     The Y coordinate of the upper-left corner of the rectangle to draw.
   5263     *
   5264     * @param {!number} w
   5265     *     The width of the rectangle to draw.
   5266     *
   5267     * @param {!number} h
   5268     *     The height of the rectangle to draw.
   5269     */
   5270    this.rect = function(layer, x, y, w, h) {
   5271        scheduleTask(function __display_rect() {
   5272            layer.rect(x, y, w, h);
   5273        });
   5274    };
   5275
   5276    /**
   5277     * Clip all future drawing operations by the current path. The current path
   5278     * is implicitly closed. The current path can continue to be reused
   5279     * for other operations (such as fillColor()) but a new path will be started
   5280     * once a path drawing operation (path() or rect()) is used.
   5281     * 
   5282     * @param {!Guacamole.Layer} layer
   5283     *     The layer to affect.
   5284     */
   5285    this.clip = function(layer) {
   5286        scheduleTask(function __display_clip() {
   5287            layer.clip();
   5288        });
   5289    };
   5290
   5291    /**
   5292     * Stroke the current path with the specified color. The current path
   5293     * is implicitly closed. The current path can continue to be reused
   5294     * for other operations (such as clip()) but a new path will be started
   5295     * once a path drawing operation (path() or rect()) is used.
   5296     *
   5297     * @param {!Guacamole.Layer} layer
   5298     *     The layer to draw upon.
   5299     *
   5300     * @param {!string} cap
   5301     *     The line cap style. Can be "round", "square", or "butt".
   5302     *
   5303     * @param {!string} join
   5304     *     The line join style. Can be "round", "bevel", or "miter".
   5305     *
   5306     * @param {!number} thickness
   5307     *     The line thickness in pixels.
   5308     *
   5309     * @param {!number} r
   5310     *     The red component of the color to fill.
   5311     *
   5312     * @param {!number} g
   5313     *     The green component of the color to fill.
   5314     *
   5315     * @param {!number} b
   5316     *     The blue component of the color to fill.
   5317     *
   5318     * @param {!number} a
   5319     *     The alpha component of the color to fill.
   5320     */
   5321    this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) {
   5322        scheduleTask(function __display_strokeColor() {
   5323            layer.strokeColor(cap, join, thickness, r, g, b, a);
   5324        });
   5325    };
   5326
   5327    /**
   5328     * Fills the current path with the specified color. The current path
   5329     * is implicitly closed. The current path can continue to be reused
   5330     * for other operations (such as clip()) but a new path will be started
   5331     * once a path drawing operation (path() or rect()) is used.
   5332     * 
   5333     * @param {!Guacamole.Layer} layer
   5334     *     The layer to draw upon.
   5335     *
   5336     * @param {!number} r
   5337     *     The red component of the color to fill.
   5338     *
   5339     * @param {!number} g
   5340     *     The green component of the color to fill.
   5341     *
   5342     * @param {!number} b
   5343     *     The blue component of the color to fill.
   5344     *
   5345     * @param {!number} a
   5346     *     The alpha component of the color to fill.
   5347     */
   5348    this.fillColor = function(layer, r, g, b, a) {
   5349        scheduleTask(function __display_fillColor() {
   5350            layer.fillColor(r, g, b, a);
   5351        });
   5352    };
   5353
   5354    /**
   5355     * Stroke the current path with the image within the specified layer. The
   5356     * image data will be tiled infinitely within the stroke. The current path
   5357     * is implicitly closed. The current path can continue to be reused
   5358     * for other operations (such as clip()) but a new path will be started
   5359     * once a path drawing operation (path() or rect()) is used.
   5360     * 
   5361     * @param {!Guacamole.Layer} layer
   5362     *     The layer to draw upon.
   5363     *
   5364     * @param {!string} cap
   5365     *     The line cap style. Can be "round", "square", or "butt".
   5366     *
   5367     * @param {!string} join
   5368     *     The line join style. Can be "round", "bevel", or "miter".
   5369     *
   5370     * @param {!number} thickness
   5371     *     The line thickness in pixels.
   5372     *
   5373     * @param {!Guacamole.Layer} srcLayer
   5374     *     The layer to use as a repeating pattern within the stroke.
   5375     */
   5376    this.strokeLayer = function(layer, cap, join, thickness, srcLayer) {
   5377        scheduleTask(function __display_strokeLayer() {
   5378            layer.strokeLayer(cap, join, thickness, srcLayer);
   5379        });
   5380    };
   5381
   5382    /**
   5383     * Fills the current path with the image within the specified layer. The
   5384     * image data will be tiled infinitely within the stroke. The current path
   5385     * is implicitly closed. The current path can continue to be reused
   5386     * for other operations (such as clip()) but a new path will be started
   5387     * once a path drawing operation (path() or rect()) is used.
   5388     * 
   5389     * @param {!Guacamole.Layer} layer
   5390     *     The layer to draw upon.
   5391     *
   5392     * @param {!Guacamole.Layer} srcLayer
   5393     *     The layer to use as a repeating pattern within the fill.
   5394     */
   5395    this.fillLayer = function(layer, srcLayer) {
   5396        scheduleTask(function __display_fillLayer() {
   5397            layer.fillLayer(srcLayer);
   5398        });
   5399    };
   5400
   5401    /**
   5402     * Push current layer state onto stack.
   5403     * 
   5404     * @param {!Guacamole.Layer} layer
   5405     *     The layer to draw upon.
   5406     */
   5407    this.push = function(layer) {
   5408        scheduleTask(function __display_push() {
   5409            layer.push();
   5410        });
   5411    };
   5412
   5413    /**
   5414     * Pop layer state off stack.
   5415     * 
   5416     * @param {!Guacamole.Layer} layer
   5417     *     The layer to draw upon.
   5418     */
   5419    this.pop = function(layer) {
   5420        scheduleTask(function __display_pop() {
   5421            layer.pop();
   5422        });
   5423    };
   5424
   5425    /**
   5426     * Reset the layer, clearing the stack, the current path, and any transform
   5427     * matrix.
   5428     * 
   5429     * @param {!Guacamole.Layer} layer
   5430     *     The layer to draw upon.
   5431     */
   5432    this.reset = function(layer) {
   5433        scheduleTask(function __display_reset() {
   5434            layer.reset();
   5435        });
   5436    };
   5437
   5438    /**
   5439     * Sets the given affine transform (defined with six values from the
   5440     * transform's matrix).
   5441     *
   5442     * @param {!Guacamole.Layer} layer
   5443     *     The layer to modify.
   5444     *
   5445     * @param {!number} a
   5446     *     The first value in the affine transform's matrix.
   5447     *
   5448     * @param {!number} b
   5449     *     The second value in the affine transform's matrix.
   5450     *
   5451     * @param {!number} c
   5452     *     The third value in the affine transform's matrix.
   5453     *
   5454     * @param {!number} d
   5455     *     The fourth value in the affine transform's matrix.
   5456     *
   5457     * @param {!number} e
   5458     *     The fifth value in the affine transform's matrix.
   5459     *
   5460     * @param {!number} f
   5461     *     The sixth value in the affine transform's matrix.
   5462     */
   5463    this.setTransform = function(layer, a, b, c, d, e, f) {
   5464        scheduleTask(function __display_setTransform() {
   5465            layer.setTransform(a, b, c, d, e, f);
   5466        });
   5467    };
   5468
   5469    /**
   5470     * Applies the given affine transform (defined with six values from the
   5471     * transform's matrix).
   5472     *
   5473     * @param {!Guacamole.Layer} layer
   5474     *     The layer to modify.
   5475     *
   5476     * @param {!number} a
   5477     *     The first value in the affine transform's matrix.
   5478     *
   5479     * @param {!number} b
   5480     *     The second value in the affine transform's matrix.
   5481     *
   5482     * @param {!number} c
   5483     *     The third value in the affine transform's matrix.
   5484     *
   5485     * @param {!number} d
   5486     *     The fourth value in the affine transform's matrix.
   5487     *
   5488     * @param {!number} e
   5489     *     The fifth value in the affine transform's matrix.
   5490     *
   5491     * @param {!number} f
   5492     *     The sixth value in the affine transform's matrix.
   5493     *
   5494     */
   5495    this.transform = function(layer, a, b, c, d, e, f) {
   5496        scheduleTask(function __display_transform() {
   5497            layer.transform(a, b, c, d, e, f);
   5498        });
   5499    };
   5500
   5501    /**
   5502     * Sets the channel mask for future operations on this Layer.
   5503     * 
   5504     * The channel mask is a Guacamole-specific compositing operation identifier
   5505     * with a single bit representing each of four channels (in order): source
   5506     * image where destination transparent, source where destination opaque,
   5507     * destination where source transparent, and destination where source
   5508     * opaque.
   5509     * 
   5510     * @param {!Guacamole.Layer} layer
   5511     *     The layer to modify.
   5512     *
   5513     * @param {!number} mask
   5514     *     The channel mask for future operations on this Layer.
   5515     */
   5516    this.setChannelMask = function(layer, mask) {
   5517        scheduleTask(function __display_setChannelMask() {
   5518            layer.setChannelMask(mask);
   5519        });
   5520    };
   5521
   5522    /**
   5523     * Sets the miter limit for stroke operations using the miter join. This
   5524     * limit is the maximum ratio of the size of the miter join to the stroke
   5525     * width. If this ratio is exceeded, the miter will not be drawn for that
   5526     * joint of the path.
   5527     * 
   5528     * @param {!Guacamole.Layer} layer
   5529     *     The layer to modify.
   5530     *
   5531     * @param {!number} limit
   5532     *     The miter limit for stroke operations using the miter join.
   5533     */
   5534    this.setMiterLimit = function(layer, limit) {
   5535        scheduleTask(function __display_setMiterLimit() {
   5536            layer.setMiterLimit(limit);
   5537        });
   5538    };
   5539
   5540    /**
   5541     * Removes the given layer container entirely, such that it is no longer
   5542     * contained within its parent layer, if any.
   5543     *
   5544     * @param {!Guacamole.Display.VisibleLayer} layer
   5545     *     The layer being removed from its parent.
   5546     */
   5547    this.dispose = function dispose(layer) {
   5548        scheduleTask(function disposeLayer() {
   5549            layer.dispose();
   5550        });
   5551    };
   5552
   5553    /**
   5554     * Applies the given affine transform (defined with six values from the
   5555     * transform's matrix) to the given layer.
   5556     *
   5557     * @param {!Guacamole.Display.VisibleLayer} layer
   5558     *     The layer being distorted.
   5559     *
   5560     * @param {!number} a
   5561     *     The first value in the affine transform's matrix.
   5562     *
   5563     * @param {!number} b
   5564     *     The second value in the affine transform's matrix.
   5565     *
   5566     * @param {!number} c
   5567     *     The third value in the affine transform's matrix.
   5568     *
   5569     * @param {!number} d
   5570     *     The fourth value in the affine transform's matrix.
   5571     *
   5572     * @param {!number} e
   5573     *     The fifth value in the affine transform's matrix.
   5574     *
   5575     * @param {!number} f
   5576     *     The sixth value in the affine transform's matrix.
   5577     */
   5578    this.distort = function distort(layer, a, b, c, d, e, f) {
   5579        scheduleTask(function distortLayer() {
   5580            layer.distort(a, b, c, d, e, f);
   5581        });
   5582    };
   5583
   5584    /**
   5585     * Moves the upper-left corner of the given layer to the given X and Y
   5586     * coordinate, sets the Z stacking order, and reparents the layer
   5587     * to the given parent layer.
   5588     *
   5589     * @param {!Guacamole.Display.VisibleLayer} layer
   5590     *     The layer being moved.
   5591     *
   5592     * @param {!Guacamole.Display.VisibleLayer} parent
   5593     *     The parent to set.
   5594     *
   5595     * @param {!number} x
   5596     *     The X coordinate to move to.
   5597     *
   5598     * @param {!number} y
   5599     *     The Y coordinate to move to.
   5600     *
   5601     * @param {!number} z
   5602     *     The Z coordinate to move to.
   5603     */
   5604    this.move = function move(layer, parent, x, y, z) {
   5605        scheduleTask(function moveLayer() {
   5606            layer.move(parent, x, y, z);
   5607        });
   5608    };
   5609
   5610    /**
   5611     * Sets the opacity of the given layer to the given value, where 255 is
   5612     * fully opaque and 0 is fully transparent.
   5613     *
   5614     * @param {!Guacamole.Display.VisibleLayer} layer
   5615     *     The layer whose opacity should be set.
   5616     *
   5617     * @param {!number} alpha
   5618     *     The opacity to set.
   5619     */
   5620    this.shade = function shade(layer, alpha) {
   5621        scheduleTask(function shadeLayer() {
   5622            layer.shade(alpha);
   5623        });
   5624    };
   5625
   5626    /**
   5627     * Sets the scale of the client display element such that it renders at
   5628     * a relatively smaller or larger size, without affecting the true
   5629     * resolution of the display.
   5630     *
   5631     * @param {!number} scale
   5632     *     The scale to resize to, where 1.0 is normal size (1:1 scale).
   5633     */
   5634    this.scale = function(scale) {
   5635
   5636        display.style.transform =
   5637        display.style.WebkitTransform =
   5638        display.style.MozTransform =
   5639        display.style.OTransform =
   5640        display.style.msTransform =
   5641
   5642            "scale(" + scale + "," + scale + ")";
   5643
   5644        displayScale = scale;
   5645
   5646        // Update bounds size
   5647        bounds.style.width = (displayWidth*displayScale) + "px";
   5648        bounds.style.height = (displayHeight*displayScale) + "px";
   5649
   5650    };
   5651
   5652    /**
   5653     * Returns the scale of the display.
   5654     *
   5655     * @return {!number}
   5656     *     The scale of the display.
   5657     */
   5658    this.getScale = function() {
   5659        return displayScale;
   5660    };
   5661
   5662    /**
   5663     * Returns a canvas element containing the entire display, with all child
   5664     * layers composited within.
   5665     *
   5666     * @return {!HTMLCanvasElement}
   5667     *     A new canvas element containing a copy of the display.
   5668     */
   5669    this.flatten = function() {
   5670       
   5671        // Get destination canvas
   5672        var canvas = document.createElement("canvas");
   5673        canvas.width = default_layer.width;
   5674        canvas.height = default_layer.height;
   5675
   5676        var context = canvas.getContext("2d");
   5677
   5678        // Returns sorted array of children
   5679        function get_children(layer) {
   5680
   5681            // Build array of children
   5682            var children = [];
   5683            for (var index in layer.children)
   5684                children.push(layer.children[index]);
   5685
   5686            // Sort
   5687            children.sort(function children_comparator(a, b) {
   5688
   5689                // Compare based on Z order
   5690                var diff = a.z - b.z;
   5691                if (diff !== 0)
   5692                    return diff;
   5693
   5694                // If Z order identical, use document order
   5695                var a_element = a.getElement();
   5696                var b_element = b.getElement();
   5697                var position = b_element.compareDocumentPosition(a_element);
   5698
   5699                if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1;
   5700                if (position & Node.DOCUMENT_POSITION_FOLLOWING) return  1;
   5701
   5702                // Otherwise, assume same
   5703                return 0;
   5704
   5705            });
   5706
   5707            // Done
   5708            return children;
   5709
   5710        }
   5711
   5712        // Draws the contents of the given layer at the given coordinates
   5713        function draw_layer(layer, x, y) {
   5714
   5715            // Draw layer
   5716            if (layer.width > 0 && layer.height > 0) {
   5717
   5718                // Save and update alpha
   5719                var initial_alpha = context.globalAlpha;
   5720                context.globalAlpha *= layer.alpha / 255.0;
   5721
   5722                // Copy data
   5723                context.drawImage(layer.getCanvas(), x, y);
   5724
   5725                // Draw all children
   5726                var children = get_children(layer);
   5727                for (var i=0; i<children.length; i++) {
   5728                    var child = children[i];
   5729                    draw_layer(child, x + child.x, y + child.y);
   5730                }
   5731
   5732                // Restore alpha
   5733                context.globalAlpha = initial_alpha;
   5734
   5735            }
   5736
   5737        }
   5738
   5739        // Draw default layer and all children
   5740        draw_layer(default_layer, 0, 0);
   5741
   5742        // Return new canvas copy
   5743        return canvas;
   5744        
   5745    };
   5746
   5747};
   5748
   5749/**
   5750 * Simple container for Guacamole.Layer, allowing layers to be easily
   5751 * repositioned and nested. This allows certain operations to be accelerated
   5752 * through DOM manipulation, rather than raster operations.
   5753 * 
   5754 * @constructor
   5755 * @augments Guacamole.Layer
   5756 * @param {!number} width
   5757 *     The width of the Layer, in pixels. The canvas element backing this Layer
   5758 *     will be given this width.
   5759 *
   5760 * @param {!number} height
   5761 *     The height of the Layer, in pixels. The canvas element backing this
   5762 *     Layer will be given this height.
   5763 */
   5764Guacamole.Display.VisibleLayer = function(width, height) {
   5765
   5766    Guacamole.Layer.apply(this, [width, height]);
   5767
   5768    /**
   5769     * Reference to this layer.
   5770     *
   5771     * @private
   5772     * @type {!Guacamole.Display.Layer}
   5773     */
   5774    var layer = this;
   5775
   5776    /**
   5777     * Identifier which uniquely identifies this layer. This is COMPLETELY
   5778     * UNRELATED to the index of the underlying layer, which is specific
   5779     * to the Guacamole protocol, and not relevant at this level.
   5780     * 
   5781     * @private
   5782     * @type {!number}
   5783     */
   5784    this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++;
   5785
   5786    /**
   5787     * The opacity of the layer container, where 255 is fully opaque and 0 is
   5788     * fully transparent.
   5789     *
   5790     * @type {!number}
   5791     */
   5792    this.alpha = 0xFF;
   5793
   5794    /**
   5795     * X coordinate of the upper-left corner of this layer container within
   5796     * its parent, in pixels.
   5797     *
   5798     * @type {!number}
   5799     */
   5800    this.x = 0;
   5801
   5802    /**
   5803     * Y coordinate of the upper-left corner of this layer container within
   5804     * its parent, in pixels.
   5805     *
   5806     * @type {!number}
   5807     */
   5808    this.y = 0;
   5809
   5810    /**
   5811     * Z stacking order of this layer relative to other sibling layers.
   5812     *
   5813     * @type {!number}
   5814     */
   5815    this.z = 0;
   5816
   5817    /**
   5818     * The affine transformation applied to this layer container. Each element
   5819     * corresponds to a value from the transformation matrix, with the first
   5820     * three values being the first row, and the last three values being the
   5821     * second row. There are six values total.
   5822     * 
   5823     * @type {!number[]}
   5824     */
   5825    this.matrix = [1, 0, 0, 1, 0, 0];
   5826
   5827    /**
   5828     * The parent layer container of this layer, if any.
   5829     * @type {Guacamole.Display.VisibleLayer}
   5830     */
   5831    this.parent = null;
   5832
   5833    /**
   5834     * Set of all children of this layer, indexed by layer index. This object
   5835     * will have one property per child.
   5836     *
   5837     * @type {!Object.<number, Guacamole.Display.VisibleLayer>}
   5838     */
   5839    this.children = {};
   5840
   5841    // Set layer position
   5842    var canvas = layer.getCanvas();
   5843    canvas.style.position = "absolute";
   5844    canvas.style.left = "0px";
   5845    canvas.style.top = "0px";
   5846
   5847    // Create div with given size
   5848    var div = document.createElement("div");
   5849    div.appendChild(canvas);
   5850    div.style.width = width + "px";
   5851    div.style.height = height + "px";
   5852    div.style.position = "absolute";
   5853    div.style.left = "0px";
   5854    div.style.top = "0px";
   5855    div.style.overflow = "hidden";
   5856
   5857    /**
   5858     * Superclass resize() function.
   5859     * @private
   5860     */
   5861    var __super_resize = this.resize;
   5862
   5863    this.resize = function(width, height) {
   5864
   5865        // Resize containing div
   5866        div.style.width = width + "px";
   5867        div.style.height = height + "px";
   5868
   5869        __super_resize(width, height);
   5870
   5871    };
   5872  
   5873    /**
   5874     * Returns the element containing the canvas and any other elements
   5875     * associated with this layer.
   5876     *
   5877     * @returns {!Element}
   5878     *     The element containing this layer's canvas.
   5879     */
   5880    this.getElement = function() {
   5881        return div;
   5882    };
   5883
   5884    /**
   5885     * The translation component of this layer's transform.
   5886     *
   5887     * @private
   5888     * @type {!string}
   5889     */
   5890    var translate = "translate(0px, 0px)"; // (0, 0)
   5891
   5892    /**
   5893     * The arbitrary matrix component of this layer's transform.
   5894     *
   5895     * @private
   5896     * @type {!string}
   5897     */
   5898    var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity
   5899
   5900    /**
   5901     * Moves the upper-left corner of this layer to the given X and Y
   5902     * coordinate.
   5903     * 
   5904     * @param {!number} x
   5905     *     The X coordinate to move to.
   5906     *
   5907     * @param {!number} y
   5908     *     The Y coordinate to move to.
   5909     */
   5910    this.translate = function(x, y) {
   5911
   5912        layer.x = x;
   5913        layer.y = y;
   5914
   5915        // Generate translation
   5916        translate = "translate("
   5917                        + x + "px,"
   5918                        + y + "px)";
   5919
   5920        // Set layer transform 
   5921        div.style.transform =
   5922        div.style.WebkitTransform =
   5923        div.style.MozTransform =
   5924        div.style.OTransform =
   5925        div.style.msTransform =
   5926
   5927            translate + " " + matrix;
   5928
   5929    };
   5930
   5931    /**
   5932     * Moves the upper-left corner of this VisibleLayer to the given X and Y
   5933     * coordinate, sets the Z stacking order, and reparents this VisibleLayer
   5934     * to the given VisibleLayer.
   5935     * 
   5936     * @param {!Guacamole.Display.VisibleLayer} parent
   5937     *     The parent to set.
   5938     *
   5939     * @param {!number} x
   5940     *     The X coordinate to move to.
   5941     *
   5942     * @param {!number} y
   5943     *     The Y coordinate to move to.
   5944     *
   5945     * @param {!number} z
   5946     *     The Z coordinate to move to.
   5947     */
   5948    this.move = function(parent, x, y, z) {
   5949
   5950        // Set parent if necessary
   5951        if (layer.parent !== parent) {
   5952
   5953            // Maintain relationship
   5954            if (layer.parent)
   5955                delete layer.parent.children[layer.__unique_id];
   5956            layer.parent = parent;
   5957            parent.children[layer.__unique_id] = layer;
   5958
   5959            // Reparent element
   5960            var parent_element = parent.getElement();
   5961            parent_element.appendChild(div);
   5962
   5963        }
   5964
   5965        // Set location
   5966        layer.translate(x, y);
   5967        layer.z = z;
   5968        div.style.zIndex = z;
   5969
   5970    };
   5971
   5972    /**
   5973     * Sets the opacity of this layer to the given value, where 255 is fully
   5974     * opaque and 0 is fully transparent.
   5975     * 
   5976     * @param {!number} a
   5977     *     The opacity to set.
   5978     */
   5979    this.shade = function(a) {
   5980        layer.alpha = a;
   5981        div.style.opacity = a/255.0;
   5982    };
   5983
   5984    /**
   5985     * Removes this layer container entirely, such that it is no longer
   5986     * contained within its parent layer, if any.
   5987     */
   5988    this.dispose = function() {
   5989
   5990        // Remove from parent container
   5991        if (layer.parent) {
   5992            delete layer.parent.children[layer.__unique_id];
   5993            layer.parent = null;
   5994        }
   5995
   5996        // Remove from parent element
   5997        if (div.parentNode)
   5998            div.parentNode.removeChild(div);
   5999        
   6000    };
   6001
   6002    /**
   6003     * Applies the given affine transform (defined with six values from the
   6004     * transform's matrix).
   6005     *
   6006     * @param {!number} a
   6007     *     The first value in the affine transform's matrix.
   6008     *
   6009     * @param {!number} b
   6010     *     The second value in the affine transform's matrix.
   6011     *
   6012     * @param {!number} c
   6013     *     The third value in the affine transform's matrix.
   6014     *
   6015     * @param {!number} d
   6016     *     The fourth value in the affine transform's matrix.
   6017     *
   6018     * @param {!number} e
   6019     *     The fifth value in the affine transform's matrix.
   6020     *
   6021     * @param {!number} f
   6022     *     The sixth value in the affine transform's matrix.
   6023     */
   6024    this.distort = function(a, b, c, d, e, f) {
   6025
   6026        // Store matrix
   6027        layer.matrix = [a, b, c, d, e, f];
   6028
   6029        // Generate matrix transformation
   6030        matrix =
   6031
   6032            /* a c e
   6033             * b d f
   6034             * 0 0 1
   6035             */
   6036    
   6037            "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
   6038
   6039        // Set layer transform 
   6040        div.style.transform =
   6041        div.style.WebkitTransform =
   6042        div.style.MozTransform =
   6043        div.style.OTransform =
   6044        div.style.msTransform =
   6045
   6046            translate + " " + matrix;
   6047
   6048    };
   6049
   6050};
   6051
   6052/**
   6053 * The next identifier to be assigned to the layer container. This identifier
   6054 * uniquely identifies each VisibleLayer, but is unrelated to the index of
   6055 * the layer, which exists at the protocol/client level only.
   6056 * 
   6057 * @private
   6058 * @type {!number}
   6059 */
   6060Guacamole.Display.VisibleLayer.__next_id = 0;
   6061
   6062/**
   6063 * A set of Guacamole display performance statistics, describing the speed at
   6064 * which the remote desktop, Guacamole server, and Guacamole client are
   6065 * rendering frames.
   6066 *
   6067 * @constructor
   6068 * @param {Guacamole.Display.Statistics|Object} [template={}]
   6069 *     The object whose properties should be copied within the new
   6070 *     Guacamole.Display.Statistics.
   6071 */
   6072Guacamole.Display.Statistics = function Statistics(template) {
   6073
   6074    template = template || {};
   6075
   6076    /**
   6077     * The amount of time that the Guacamole client is taking to render
   6078     * individual frames, in milliseconds, if known. If this value is unknown,
   6079     * such as if the there are insufficient frame statistics recorded to
   6080     * calculate this value, this will be null.
   6081     *
   6082     * @type {?number}
   6083     */
   6084    this.processingLag = template.processingLag;
   6085
   6086    /**
   6087     * The framerate of the remote desktop currently being viewed within the
   6088     * relevant Gucamole.Display, independent of Guacamole, in frames per
   6089     * second. This represents the speed at which the remote desktop is
   6090     * producing frame data for the Guacamole server to consume. If this
   6091     * value is unknown, such as if the remote desktop server does not actually
   6092     * define frame boundaries, this will be null.
   6093     *
   6094     * @type {?number}
   6095     */
   6096    this.desktopFps = template.desktopFps;
   6097
   6098    /**
   6099     * The rate at which the Guacamole server is generating frames for the
   6100     * Guacamole client to consume, in frames per second. If the Guacamole
   6101     * server is correctly adjusting for variance in client/browser processing
   6102     * power, this rate should closely match the client rate, and should remain
   6103     * independent of any network latency. If this value is unknown, such as if
   6104     * the there are insufficient frame statistics recorded to calculate this
   6105     * value, this will be null.
   6106     *
   6107     * @type {?number}
   6108     */
   6109    this.serverFps = template.serverFps;
   6110
   6111    /**
   6112     * The rate at which the Guacamole client is consuming frames generated by
   6113     * the Guacamole server, in frames per second. If the Guacamole server is
   6114     * correctly adjusting for variance in client/browser processing power,
   6115     * this rate should closely match the server rate, regardless of any
   6116     * latency on the network between the server and client. If this value is
   6117     * unknown, such as if the there are insufficient frame statistics recorded
   6118     * to calculate this value, this will be null.
   6119     *
   6120     * @type {?number}
   6121     */
   6122    this.clientFps = template.clientFps;
   6123
   6124    /**
   6125     * The rate at which the Guacamole server is dropping or combining frames
   6126     * received from the remote desktop server to compensate for variance in
   6127     * client/browser processing power, in frames per second. This value may
   6128     * also be non-zero if the server is compensating for variances in its own
   6129     * processing power, or relative slowness in image compression vs. the rate
   6130     * that inbound frames are received. If this value is unknown, such as if
   6131     * the remote desktop server does not actually define frame boundaries,
   6132     * this will be null.
   6133     */
   6134    this.dropRate = template.dropRate;
   6135
   6136};
   6137/*
   6138 * Licensed to the Apache Software Foundation (ASF) under one
   6139 * or more contributor license agreements.  See the NOTICE file
   6140 * distributed with this work for additional information
   6141 * regarding copyright ownership.  The ASF licenses this file
   6142 * to you under the Apache License, Version 2.0 (the
   6143 * "License"); you may not use this file except in compliance
   6144 * with the License.  You may obtain a copy of the License at
   6145 *
   6146 *   http://www.apache.org/licenses/LICENSE-2.0
   6147 *
   6148 * Unless required by applicable law or agreed to in writing,
   6149 * software distributed under the License is distributed on an
   6150 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6151 * KIND, either express or implied.  See the License for the
   6152 * specific language governing permissions and limitations
   6153 * under the License.
   6154 */
   6155
   6156var Guacamole = Guacamole || {};
   6157
   6158/**
   6159 * An arbitrary event, emitted by a {@link Guacamole.Event.Target}. This object
   6160 * should normally serve as the base class for a different object that is more
   6161 * specific to the event type.
   6162 *
   6163 * @constructor
   6164 * @param {!string} type
   6165 *     The unique name of this event type.
   6166 */
   6167Guacamole.Event = function Event(type) {
   6168
   6169    /**
   6170     * The unique name of this event type.
   6171     *
   6172     * @type {!string}
   6173     */
   6174    this.type = type;
   6175
   6176    /**
   6177     * An arbitrary timestamp in milliseconds, indicating this event's
   6178     * position in time relative to other events.
   6179     *
   6180     * @type {!number}
   6181     */
   6182    this.timestamp = new Date().getTime();
   6183
   6184    /**
   6185     * Returns the number of milliseconds elapsed since this event was created.
   6186     *
   6187     * @return {!number}
   6188     *     The number of milliseconds elapsed since this event was created.
   6189     */
   6190    this.getAge = function getAge() {
   6191        return new Date().getTime() - this.timestamp;
   6192    };
   6193
   6194    /**
   6195     * Requests that the legacy event handler associated with this event be
   6196     * invoked on the given event target. This function will be invoked
   6197     * automatically by implementations of {@link Guacamole.Event.Target}
   6198     * whenever {@link Guacamole.Event.Target#emit emit()} is invoked.
   6199     * <p>
   6200     * Older versions of Guacamole relied on single event handlers with the
   6201     * prefix "on", such as "onmousedown" or "onkeyup". If a Guacamole.Event
   6202     * implementation is replacing the event previously represented by one of
   6203     * these handlers, this function gives the implementation the opportunity
   6204     * to provide backward compatibility with the old handler.
   6205     * <p>
   6206     * Unless overridden, this function does nothing.
   6207     *
   6208     * @param {!Guacamole.Event.Target} eventTarget
   6209     *     The {@link Guacamole.Event.Target} that emitted this event.
   6210     */
   6211    this.invokeLegacyHandler = function invokeLegacyHandler(eventTarget) {
   6212        // Do nothing
   6213    };
   6214
   6215};
   6216
   6217/**
   6218 * A {@link Guacamole.Event} that may relate to one or more DOM events.
   6219 * Continued propagation and default behavior of the related DOM events may be
   6220 * prevented with {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()}
   6221 * and {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()}
   6222 * respectively.
   6223 *
   6224 * @constructor
   6225 * @augments Guacamole.Event
   6226 *
   6227 * @param {!string} type
   6228 *     The unique name of this event type.
   6229 *
   6230 * @param {Event|Event[]} [events=[]]
   6231 *     The DOM events that are related to this event, if any. Future calls to
   6232 *     {@link Guacamole.Event.DOMEvent#preventDefault preventDefault()} and
   6233 *     {@link Guacamole.Event.DOMEvent#stopPropagation stopPropagation()} will
   6234 *     affect these events.
   6235 */
   6236Guacamole.Event.DOMEvent = function DOMEvent(type, events) {
   6237
   6238    Guacamole.Event.call(this, type);
   6239
   6240    // Default to empty array
   6241    events = events || [];
   6242
   6243    // Automatically wrap non-array single Event in an array
   6244    if (!Array.isArray(events))
   6245        events = [ events ];
   6246
   6247    /**
   6248     * Requests that the default behavior of related DOM events be prevented.
   6249     * Whether this request will be honored by the browser depends on the
   6250     * nature of those events and the timing of the request.
   6251     */
   6252    this.preventDefault = function preventDefault() {
   6253        events.forEach(function applyPreventDefault(event) {
   6254            if (event.preventDefault) event.preventDefault();
   6255            event.returnValue = false;
   6256        });
   6257    };
   6258
   6259    /**
   6260     * Stops further propagation of related events through the DOM. Only events
   6261     * that are directly related to this event will be stopped.
   6262     */
   6263    this.stopPropagation = function stopPropagation() {
   6264        events.forEach(function applyStopPropagation(event) {
   6265            event.stopPropagation();
   6266        });
   6267    };
   6268
   6269};
   6270
   6271/**
   6272 * Convenience function for cancelling all further processing of a given DOM
   6273 * event. Invoking this function prevents the default behavior of the event and
   6274 * stops any further propagation.
   6275 *
   6276 * @param {!Event} event
   6277 *     The DOM event to cancel.
   6278 */
   6279Guacamole.Event.DOMEvent.cancelEvent = function cancelEvent(event) {
   6280    event.stopPropagation();
   6281    if (event.preventDefault) event.preventDefault();
   6282    event.returnValue = false;
   6283};
   6284
   6285/**
   6286 * An object which can dispatch {@link Guacamole.Event} objects. Listeners
   6287 * registered with {@link Guacamole.Event.Target#on on()} will automatically
   6288 * be invoked based on the type of {@link Guacamole.Event} passed to
   6289 * {@link Guacamole.Event.Target#dispatch dispatch()}. It is normally
   6290 * subclasses of Guacamole.Event.Target that will dispatch events, and usages
   6291 * of those subclasses that will catch dispatched events with on().
   6292 *
   6293 * @constructor
   6294 */
   6295Guacamole.Event.Target = function Target() {
   6296
   6297    /**
   6298     * A callback function which handles an event dispatched by an event
   6299     * target.
   6300     *
   6301     * @callback Guacamole.Event.Target~listener
   6302     * @param {!Guacamole.Event} event
   6303     *     The event that was dispatched.
   6304     *
   6305     * @param {!Guacamole.Event.Target} target
   6306     *     The object that dispatched the event.
   6307     */
   6308
   6309    /**
   6310     * All listeners (callback functions) registered for each event type passed
   6311     * to {@link Guacamole.Event.Targer#on on()}.
   6312     *
   6313     * @private
   6314     * @type {!Object.<string, Guacamole.Event.Target~listener[]>}
   6315     */
   6316    var listeners = {};
   6317
   6318    /**
   6319     * Registers a listener for events having the given type, as dictated by
   6320     * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
   6321     * provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
   6322     *
   6323     * @param {!string} type
   6324     *     The unique name of this event type.
   6325     *
   6326     * @param {!Guacamole.Event.Target~listener} listener
   6327     *     The function to invoke when an event having the given type is
   6328     *     dispatched. The {@link Guacamole.Event} object provided to
   6329     *     {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
   6330     *     this function, along with the dispatching Guacamole.Event.Target.
   6331     */
   6332    this.on = function on(type, listener) {
   6333
   6334        var relevantListeners = listeners[type];
   6335        if (!relevantListeners)
   6336            listeners[type] = relevantListeners = [];
   6337
   6338        relevantListeners.push(listener);
   6339
   6340    };
   6341
   6342    /**
   6343     * Registers a listener for events having the given types, as dictated by
   6344     * the {@link Guacamole.Event#type type} property of {@link Guacamole.Event}
   6345     * provided to {@link Guacamole.Event.Target#dispatch dispatch()}.
   6346     * <p>
   6347     * Invoking this function is equivalent to manually invoking
   6348     * {@link Guacamole.Event.Target#on on()} for each of the provided types.
   6349     *
   6350     * @param {!string[]} types
   6351     *     The unique names of the event types to associate with the given
   6352     *     listener.
   6353     *
   6354     * @param {!Guacamole.Event.Target~listener} listener
   6355     *     The function to invoke when an event having any of the given types
   6356     *     is dispatched. The {@link Guacamole.Event} object provided to
   6357     *     {@link Guacamole.Event.Target#dispatch dispatch()} will be passed to
   6358     *     this function, along with the dispatching Guacamole.Event.Target.
   6359     */
   6360    this.onEach = function onEach(types, listener) {
   6361        types.forEach(function addListener(type) {
   6362            this.on(type, listener);
   6363        }, this);
   6364    };
   6365
   6366    /**
   6367     * Dispatches the given event, invoking all event handlers registered with
   6368     * this Guacamole.Event.Target for that event's
   6369     * {@link Guacamole.Event#type type}.
   6370     *
   6371     * @param {!Guacamole.Event} event
   6372     *     The event to dispatch.
   6373     */
   6374    this.dispatch = function dispatch(event) {
   6375
   6376        // Invoke any relevant legacy handler for the event
   6377        event.invokeLegacyHandler(this);
   6378
   6379        // Invoke all registered listeners
   6380        var relevantListeners = listeners[event.type];
   6381        if (relevantListeners) {
   6382            for (var i = 0; i < relevantListeners.length; i++) {
   6383                relevantListeners[i](event, this);
   6384            }
   6385        }
   6386
   6387    };
   6388
   6389    /**
   6390     * Unregisters a listener that was previously registered with
   6391     * {@link Guacamole.Event.Target#on on()} or
   6392     * {@link Guacamole.Event.Target#onEach onEach()}. If no such listener was
   6393     * registered, this function has no effect. If multiple copies of the same
   6394     * listener were registered, the first listener still registered will be
   6395     * removed.
   6396     *
   6397     * @param {!string} type
   6398     *     The unique name of the event type handled by the listener being
   6399     *     removed.
   6400     *
   6401     * @param {!Guacamole.Event.Target~listener} listener
   6402     *     The listener function previously provided to
   6403     *     {@link Guacamole.Event.Target#on on()}or
   6404     *     {@link Guacamole.Event.Target#onEach onEach()}.
   6405     *
   6406     * @returns {!boolean}
   6407     *     true if the specified listener was removed, false otherwise.
   6408     */
   6409    this.off = function off(type, listener) {
   6410
   6411        var relevantListeners = listeners[type];
   6412        if (!relevantListeners)
   6413            return false;
   6414
   6415        for (var i = 0; i < relevantListeners.length; i++) {
   6416            if (relevantListeners[i] === listener) {
   6417                relevantListeners.splice(i, 1);
   6418                return true;
   6419            }
   6420        }
   6421
   6422        return false;
   6423
   6424    };
   6425
   6426    /**
   6427     * Unregisters listeners that were previously registered with
   6428     * {@link Guacamole.Event.Target#on on()} or
   6429     * {@link Guacamole.Event.Target#onEach onEach()}. If no such listeners
   6430     * were registered, this function has no effect. If multiple copies of the
   6431     * same listener were registered for the same event type, the first
   6432     * listener still registered will be removed.
   6433     * <p>
   6434     * Invoking this function is equivalent to manually invoking
   6435     * {@link Guacamole.Event.Target#off off()} for each of the provided types.
   6436     *
   6437     * @param {!string[]} types
   6438     *     The unique names of the event types handled by the listeners being
   6439     *     removed.
   6440     *
   6441     * @param {!Guacamole.Event.Target~listener} listener
   6442     *     The listener function previously provided to
   6443     *     {@link Guacamole.Event.Target#on on()} or
   6444     *     {@link Guacamole.Event.Target#onEach onEach()}.
   6445     *
   6446     * @returns {!boolean}
   6447     *     true if any of the specified listeners were removed, false
   6448     *     otherwise.
   6449     */
   6450    this.offEach = function offEach(types, listener) {
   6451
   6452        var changed = false;
   6453
   6454        types.forEach(function removeListener(type) {
   6455            changed |= this.off(type, listener);
   6456        }, this);
   6457
   6458        return changed;
   6459
   6460    };
   6461
   6462};
   6463/*
   6464 * Licensed to the Apache Software Foundation (ASF) under one
   6465 * or more contributor license agreements.  See the NOTICE file
   6466 * distributed with this work for additional information
   6467 * regarding copyright ownership.  The ASF licenses this file
   6468 * to you under the Apache License, Version 2.0 (the
   6469 * "License"); you may not use this file except in compliance
   6470 * with the License.  You may obtain a copy of the License at
   6471 *
   6472 *   http://www.apache.org/licenses/LICENSE-2.0
   6473 *
   6474 * Unless required by applicable law or agreed to in writing,
   6475 * software distributed under the License is distributed on an
   6476 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6477 * KIND, either express or implied.  See the License for the
   6478 * specific language governing permissions and limitations
   6479 * under the License.
   6480 */
   6481
   6482var Guacamole = Guacamole || {};
   6483
   6484/**
   6485 * A hidden input field which attempts to keep itself focused at all times,
   6486 * except when another input field has been intentionally focused, whether
   6487 * programatically or by the user. The actual underlying input field, returned
   6488 * by getElement(), may be used as a reliable source of keyboard-related events,
   6489 * particularly composition and input events which may require a focused input
   6490 * field to be dispatched at all.
   6491 *
   6492 * @constructor
   6493 */
   6494Guacamole.InputSink = function InputSink() {
   6495
   6496    /**
   6497     * Reference to this instance of Guacamole.InputSink.
   6498     *
   6499     * @private
   6500     * @type {!Guacamole.InputSink}
   6501     */
   6502    var sink = this;
   6503
   6504    /**
   6505     * The underlying input field, styled to be invisible.
   6506     *
   6507     * @private
   6508     * @type {!Element}
   6509     */
   6510    var field = document.createElement('textarea');
   6511    field.style.position   = 'fixed';
   6512    field.style.outline    = 'none';
   6513    field.style.border     = 'none';
   6514    field.style.margin     = '0';
   6515    field.style.padding    = '0';
   6516    field.style.height     = '0';
   6517    field.style.width      = '0';
   6518    field.style.left       = '0';
   6519    field.style.bottom     = '0';
   6520    field.style.resize     = 'none';
   6521    field.style.background = 'transparent';
   6522    field.style.color      = 'transparent';
   6523
   6524    // Keep field clear when modified via normal keypresses
   6525    field.addEventListener("keypress", function clearKeypress(e) {
   6526        field.value = '';
   6527    }, false);
   6528
   6529    // Keep field clear when modofied via composition events
   6530    field.addEventListener("compositionend", function clearCompletedComposition(e) {
   6531        if (e.data)
   6532            field.value = '';
   6533    }, false);
   6534
   6535    // Keep field clear when modofied via input events
   6536    field.addEventListener("input", function clearCompletedInput(e) {
   6537        if (e.data && !e.isComposing)
   6538            field.value = '';
   6539    }, false);
   6540
   6541    // Whenever focus is gained, automatically click to ensure cursor is
   6542    // actually placed within the field (the field may simply be highlighted or
   6543    // outlined otherwise)
   6544    field.addEventListener("focus", function focusReceived() {
   6545        window.setTimeout(function deferRefocus() {
   6546            field.click();
   6547            field.select();
   6548        }, 0);
   6549    }, true);
   6550
   6551    /**
   6552     * Attempts to focus the underlying input field. The focus attempt occurs
   6553     * asynchronously, and may silently fail depending on browser restrictions.
   6554     */
   6555    this.focus = function focus() {
   6556        window.setTimeout(function deferRefocus() {
   6557            field.focus(); // Focus must be deferred to work reliably across browsers
   6558        }, 0);
   6559    };
   6560
   6561    /**
   6562     * Returns the underlying input field. This input field MUST be manually
   6563     * added to the DOM for the Guacamole.InputSink to have any effect.
   6564     *
   6565     * @returns {!Element}
   6566     *     The underlying input field.
   6567     */
   6568    this.getElement = function getElement() {
   6569        return field;
   6570    };
   6571
   6572    // Automatically refocus input sink if part of DOM
   6573    document.addEventListener("keydown", function refocusSink(e) {
   6574
   6575        // Do not refocus if focus is on an input field
   6576        var focused = document.activeElement;
   6577        if (focused && focused !== document.body) {
   6578
   6579            // Only consider focused input fields which are actually visible
   6580            var rect = focused.getBoundingClientRect();
   6581            if (rect.left + rect.width > 0 && rect.top + rect.height > 0)
   6582                return;
   6583
   6584        }
   6585
   6586        // Refocus input sink instead of handling click
   6587        sink.focus();
   6588
   6589    }, true);
   6590
   6591};
   6592/*
   6593 * Licensed to the Apache Software Foundation (ASF) under one
   6594 * or more contributor license agreements.  See the NOTICE file
   6595 * distributed with this work for additional information
   6596 * regarding copyright ownership.  The ASF licenses this file
   6597 * to you under the Apache License, Version 2.0 (the
   6598 * "License"); you may not use this file except in compliance
   6599 * with the License.  You may obtain a copy of the License at
   6600 *
   6601 *   http://www.apache.org/licenses/LICENSE-2.0
   6602 *
   6603 * Unless required by applicable law or agreed to in writing,
   6604 * software distributed under the License is distributed on an
   6605 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6606 * KIND, either express or implied.  See the License for the
   6607 * specific language governing permissions and limitations
   6608 * under the License.
   6609 */
   6610
   6611var Guacamole = Guacamole || {};
   6612
   6613/**
   6614 * An input stream abstraction used by the Guacamole client to facilitate
   6615 * transfer of files or other binary data.
   6616 * 
   6617 * @constructor
   6618 * @param {!Guacamole.Client} client
   6619 *     The client owning this stream.
   6620 *
   6621 * @param {!number} index
   6622 *     The index of this stream.
   6623 */
   6624Guacamole.InputStream = function(client, index) {
   6625
   6626    /**
   6627     * Reference to this stream.
   6628     *
   6629     * @private
   6630     * @type {!Guacamole.InputStream}
   6631     */
   6632    var guac_stream = this;
   6633
   6634    /**
   6635     * The index of this stream.
   6636     *
   6637     * @type {!number}
   6638     */
   6639    this.index = index;
   6640
   6641    /**
   6642     * Called when a blob of data is received.
   6643     * 
   6644     * @event
   6645     * @param {!string} data
   6646     *     The received base64 data.
   6647     */
   6648    this.onblob = null;
   6649
   6650    /**
   6651     * Called when this stream is closed.
   6652     * 
   6653     * @event
   6654     */
   6655    this.onend = null;
   6656
   6657    /**
   6658     * Acknowledges the receipt of a blob.
   6659     * 
   6660     * @param {!string} message
   6661     *     A human-readable message describing the error or status.
   6662     *
   6663     * @param {!number} code
   6664     *     The error code, if any, or 0 for success.
   6665     */
   6666    this.sendAck = function(message, code) {
   6667        client.sendAck(guac_stream.index, message, code);
   6668    };
   6669
   6670};
   6671/*
   6672 * Licensed to the Apache Software Foundation (ASF) under one
   6673 * or more contributor license agreements.  See the NOTICE file
   6674 * distributed with this work for additional information
   6675 * regarding copyright ownership.  The ASF licenses this file
   6676 * to you under the Apache License, Version 2.0 (the
   6677 * "License"); you may not use this file except in compliance
   6678 * with the License.  You may obtain a copy of the License at
   6679 *
   6680 *   http://www.apache.org/licenses/LICENSE-2.0
   6681 *
   6682 * Unless required by applicable law or agreed to in writing,
   6683 * software distributed under the License is distributed on an
   6684 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6685 * KIND, either express or implied.  See the License for the
   6686 * specific language governing permissions and limitations
   6687 * under the License.
   6688 */
   6689
   6690var Guacamole = Guacamole || {};
   6691
   6692/**
   6693 * Integer pool which returns consistently increasing integers while integers
   6694 * are in use, and previously-used integers when possible.
   6695 * @constructor 
   6696 */
   6697Guacamole.IntegerPool = function() {
   6698
   6699    /**
   6700     * Reference to this integer pool.
   6701     *
   6702     * @private
   6703     */
   6704    var guac_pool = this;
   6705
   6706    /**
   6707     * Array of available integers.
   6708     *
   6709     * @private
   6710     * @type {!number[]}
   6711     */
   6712    var pool = [];
   6713
   6714    /**
   6715     * The next integer to return if no more integers remain.
   6716     *
   6717     * @type {!number}
   6718     */
   6719    this.next_int = 0;
   6720
   6721    /**
   6722     * Returns the next available integer in the pool. If possible, a previously
   6723     * used integer will be returned.
   6724     * 
   6725     * @return {!number}
   6726     *     The next available integer.
   6727     */
   6728    this.next = function() {
   6729
   6730        // If free'd integers exist, return one of those
   6731        if (pool.length > 0)
   6732            return pool.shift();
   6733
   6734        // Otherwise, return a new integer
   6735        return guac_pool.next_int++;
   6736
   6737    };
   6738
   6739    /**
   6740     * Frees the given integer, allowing it to be reused.
   6741     * 
   6742     * @param {!number} integer
   6743     *     The integer to free.
   6744     */
   6745    this.free = function(integer) {
   6746        pool.push(integer);
   6747    };
   6748
   6749};
   6750/*
   6751 * Licensed to the Apache Software Foundation (ASF) under one
   6752 * or more contributor license agreements.  See the NOTICE file
   6753 * distributed with this work for additional information
   6754 * regarding copyright ownership.  The ASF licenses this file
   6755 * to you under the Apache License, Version 2.0 (the
   6756 * "License"); you may not use this file except in compliance
   6757 * with the License.  You may obtain a copy of the License at
   6758 *
   6759 *   http://www.apache.org/licenses/LICENSE-2.0
   6760 *
   6761 * Unless required by applicable law or agreed to in writing,
   6762 * software distributed under the License is distributed on an
   6763 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6764 * KIND, either express or implied.  See the License for the
   6765 * specific language governing permissions and limitations
   6766 * under the License.
   6767 */
   6768
   6769var Guacamole = Guacamole || {};
   6770
   6771/**
   6772 * A reader which automatically handles the given input stream, assembling all
   6773 * received blobs into a JavaScript object by appending them to each other, in
   6774 * order, and decoding the result as JSON. Note that this object will overwrite
   6775 * any installed event handlers on the given Guacamole.InputStream.
   6776 * 
   6777 * @constructor
   6778 * @param {Guacamole.InputStream} stream
   6779 *     The stream that JSON will be read from.
   6780 */
   6781Guacamole.JSONReader = function guacamoleJSONReader(stream) {
   6782
   6783    /**
   6784     * Reference to this Guacamole.JSONReader.
   6785     *
   6786     * @private
   6787     * @type {!Guacamole.JSONReader}
   6788     */
   6789    var guacReader = this;
   6790
   6791    /**
   6792     * Wrapped Guacamole.StringReader.
   6793     *
   6794     * @private
   6795     * @type {!Guacamole.StringReader}
   6796     */
   6797    var stringReader = new Guacamole.StringReader(stream);
   6798
   6799    /**
   6800     * All JSON read thus far.
   6801     *
   6802     * @private
   6803     * @type {!string}
   6804     */
   6805    var json = '';
   6806
   6807    /**
   6808     * Returns the current length of this Guacamole.JSONReader, in characters.
   6809     *
   6810     * @return {!number}
   6811     *     The current length of this Guacamole.JSONReader.
   6812     */
   6813    this.getLength = function getLength() {
   6814        return json.length;
   6815    };
   6816
   6817    /**
   6818     * Returns the contents of this Guacamole.JSONReader as a JavaScript
   6819     * object.
   6820     *
   6821     * @return {object}
   6822     *     The contents of this Guacamole.JSONReader, as parsed from the JSON
   6823     *     contents of the input stream.
   6824     */
   6825    this.getJSON = function getJSON() {
   6826        return JSON.parse(json);
   6827    };
   6828
   6829    // Append all received text
   6830    stringReader.ontext = function ontext(text) {
   6831
   6832        // Append received text
   6833        json += text;
   6834
   6835        // Call handler, if present
   6836        if (guacReader.onprogress)
   6837            guacReader.onprogress(text.length);
   6838
   6839    };
   6840
   6841    // Simply call onend when end received
   6842    stringReader.onend = function onend() {
   6843        if (guacReader.onend)
   6844            guacReader.onend();
   6845    };
   6846
   6847    /**
   6848     * Fired once for every blob of data received.
   6849     * 
   6850     * @event
   6851     * @param {!number} length
   6852     *     The number of characters received.
   6853     */
   6854    this.onprogress = null;
   6855
   6856    /**
   6857     * Fired once this stream is finished and no further data will be written.
   6858     *
   6859     * @event
   6860     */
   6861    this.onend = null;
   6862
   6863};
   6864/*
   6865 * Licensed to the Apache Software Foundation (ASF) under one
   6866 * or more contributor license agreements.  See the NOTICE file
   6867 * distributed with this work for additional information
   6868 * regarding copyright ownership.  The ASF licenses this file
   6869 * to you under the Apache License, Version 2.0 (the
   6870 * "License"); you may not use this file except in compliance
   6871 * with the License.  You may obtain a copy of the License at
   6872 *
   6873 *   http://www.apache.org/licenses/LICENSE-2.0
   6874 *
   6875 * Unless required by applicable law or agreed to in writing,
   6876 * software distributed under the License is distributed on an
   6877 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   6878 * KIND, either express or implied.  See the License for the
   6879 * specific language governing permissions and limitations
   6880 * under the License.
   6881 */
   6882
   6883var Guacamole = Guacamole || {};
   6884
   6885/**
   6886 * Provides cross-browser and cross-keyboard keyboard for a specific element.
   6887 * Browser and keyboard layout variation is abstracted away, providing events
   6888 * which represent keys as their corresponding X11 keysym.
   6889 * 
   6890 * @constructor
   6891 * @param {Element|Document} [element]
   6892 *    The Element to use to provide keyboard events. If omitted, at least one
   6893 *    Element must be manually provided through the listenTo() function for
   6894 *    the Guacamole.Keyboard instance to have any effect.
   6895 */
   6896Guacamole.Keyboard = function Keyboard(element) {
   6897
   6898    /**
   6899     * Reference to this Guacamole.Keyboard.
   6900     *
   6901     * @private
   6902     * @type {!Guacamole.Keyboard}
   6903     */
   6904    var guac_keyboard = this;
   6905
   6906    /**
   6907     * An integer value which uniquely identifies this Guacamole.Keyboard
   6908     * instance with respect to other Guacamole.Keyboard instances.
   6909     *
   6910     * @private
   6911     * @type {!number}
   6912     */
   6913    var guacKeyboardID = Guacamole.Keyboard._nextID++;
   6914
   6915    /**
   6916     * The name of the property which is added to event objects via markEvent()
   6917     * to note that they have already been handled by this Guacamole.Keyboard.
   6918     *
   6919     * @private
   6920     * @constant
   6921     * @type {!string}
   6922     */
   6923    var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
   6924
   6925    /**
   6926     * Fired whenever the user presses a key with the element associated
   6927     * with this Guacamole.Keyboard in focus.
   6928     * 
   6929     * @event
   6930     * @param {!number} keysym
   6931     *     The keysym of the key being pressed.
   6932     *
   6933     * @return {!boolean}
   6934     *     true if the key event should be allowed through to the browser,
   6935     *     false otherwise.
   6936     */
   6937    this.onkeydown = null;
   6938
   6939    /**
   6940     * Fired whenever the user releases a key with the element associated
   6941     * with this Guacamole.Keyboard in focus.
   6942     * 
   6943     * @event
   6944     * @param {!number} keysym
   6945     *     The keysym of the key being released.
   6946     */
   6947    this.onkeyup = null;
   6948
   6949    /**
   6950     * Set of known platform-specific or browser-specific quirks which must be
   6951     * accounted for to properly interpret key events, even if the only way to
   6952     * reliably detect that quirk is to platform/browser-sniff.
   6953     *
   6954     * @private
   6955     * @type {!Object.<string, boolean>}
   6956     */
   6957    var quirks = {
   6958
   6959        /**
   6960         * Whether keyup events are universally unreliable.
   6961         *
   6962         * @type {!boolean}
   6963         */
   6964        keyupUnreliable: false,
   6965
   6966        /**
   6967         * Whether the Alt key is actually a modifier for typable keys and is
   6968         * thus never used for keyboard shortcuts.
   6969         *
   6970         * @type {!boolean}
   6971         */
   6972        altIsTypableOnly: false,
   6973
   6974        /**
   6975         * Whether we can rely on receiving a keyup event for the Caps Lock
   6976         * key.
   6977         *
   6978         * @type {!boolean}
   6979         */
   6980        capsLockKeyupUnreliable: false
   6981
   6982    };
   6983
   6984    // Set quirk flags depending on platform/browser, if such information is
   6985    // available
   6986    if (navigator && navigator.platform) {
   6987
   6988        // All keyup events are unreliable on iOS (sadly)
   6989        if (navigator.platform.match(/ipad|iphone|ipod/i))
   6990            quirks.keyupUnreliable = true;
   6991
   6992        // The Alt key on Mac is never used for keyboard shortcuts, and the
   6993        // Caps Lock key never dispatches keyup events
   6994        else if (navigator.platform.match(/^mac/i)) {
   6995            quirks.altIsTypableOnly = true;
   6996            quirks.capsLockKeyupUnreliable = true;
   6997        }
   6998
   6999    }
   7000
   7001    /**
   7002     * A key event having a corresponding timestamp. This event is non-specific.
   7003     * Its subclasses should be used instead when recording specific key
   7004     * events.
   7005     *
   7006     * @private
   7007     * @constructor
   7008     * @param {KeyboardEvent} [orig]
   7009     *     The relevant DOM keyboard event.
   7010     */
   7011    var KeyEvent = function KeyEvent(orig) {
   7012
   7013        /**
   7014         * Reference to this key event.
   7015         *
   7016         * @private
   7017         * @type {!KeyEvent}
   7018         */
   7019        var key_event = this;
   7020
   7021        /**
   7022         * The JavaScript key code of the key pressed. For most events (keydown
   7023         * and keyup), this is a scancode-like value related to the position of
   7024         * the key on the US English "Qwerty" keyboard. For keypress events, 
   7025         * this is the Unicode codepoint of the character that would be typed
   7026         * by the key pressed.
   7027         *
   7028         * @type {!number}
   7029         */
   7030        this.keyCode = orig ? (orig.which || orig.keyCode) : 0;
   7031
   7032        /**
   7033         * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
   7034         * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
   7035         *
   7036         * @type {!string}
   7037         */
   7038        this.keyIdentifier = orig && orig.keyIdentifier;
   7039
   7040        /**
   7041         * The standard name of the key pressed, as defined at:
   7042         * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
   7043         * 
   7044         * @type {!string}
   7045         */
   7046        this.key = orig && orig.key;
   7047
   7048        /**
   7049         * The location on the keyboard corresponding to the key pressed, as
   7050         * defined at:
   7051         * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
   7052         * 
   7053         * @type {!number}
   7054         */
   7055        this.location = orig ? getEventLocation(orig) : 0;
   7056
   7057        /**
   7058         * The state of all local keyboard modifiers at the time this event was
   7059         * received.
   7060         *
   7061         * @type {!Guacamole.Keyboard.ModifierState}
   7062         */
   7063        this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();
   7064
   7065        /**
   7066         * An arbitrary timestamp in milliseconds, indicating this event's
   7067         * position in time relative to other events.
   7068         *
   7069         * @type {!number}
   7070         */
   7071        this.timestamp = new Date().getTime();
   7072
   7073        /**
   7074         * Whether the default action of this key event should be prevented.
   7075         *
   7076         * @type {!boolean}
   7077         */
   7078        this.defaultPrevented = false;
   7079
   7080        /**
   7081         * The keysym of the key associated with this key event, as determined
   7082         * by a best-effort guess using available event properties and keyboard
   7083         * state.
   7084         *
   7085         * @type {number}
   7086         */
   7087        this.keysym = null;
   7088
   7089        /**
   7090         * Whether the keysym value of this key event is known to be reliable.
   7091         * If false, the keysym may still be valid, but it's only a best guess,
   7092         * and future key events may be a better source of information.
   7093         *
   7094         * @type {!boolean}
   7095         */
   7096        this.reliable = false;
   7097
   7098        /**
   7099         * Returns the number of milliseconds elapsed since this event was
   7100         * received.
   7101         *
   7102         * @return {!number}
   7103         *     The number of milliseconds elapsed since this event was
   7104         *     received.
   7105         */
   7106        this.getAge = function() {
   7107            return new Date().getTime() - key_event.timestamp;
   7108        };
   7109
   7110    };
   7111
   7112    /**
   7113     * Information related to the pressing of a key, which need not be a key
   7114     * associated with a printable character. The presence or absence of any
   7115     * information within this object is browser-dependent.
   7116     *
   7117     * @private
   7118     * @constructor
   7119     * @augments Guacamole.Keyboard.KeyEvent
   7120     * @param {!KeyboardEvent} orig
   7121     *     The relevant DOM "keydown" event.
   7122     */
   7123    var KeydownEvent = function KeydownEvent(orig) {
   7124
   7125        // We extend KeyEvent
   7126        KeyEvent.call(this, orig);
   7127
   7128        // If key is known from keyCode or DOM3 alone, use that
   7129        this.keysym =  keysym_from_key_identifier(this.key, this.location)
   7130                    || keysym_from_keycode(this.keyCode, this.location);
   7131
   7132        /**
   7133         * Whether the keyup following this keydown event is known to be
   7134         * reliable. If false, we cannot rely on the keyup event to occur.
   7135         *
   7136         * @type {!boolean}
   7137         */
   7138        this.keyupReliable = !quirks.keyupUnreliable;
   7139
   7140        // DOM3 and keyCode are reliable sources if the corresponding key is
   7141        // not a printable key
   7142        if (this.keysym && !isPrintable(this.keysym))
   7143            this.reliable = true;
   7144
   7145        // Use legacy keyIdentifier as a last resort, if it looks sane
   7146        if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
   7147            this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
   7148
   7149        // If a key is pressed while meta is held down, the keyup will
   7150        // never be sent in Chrome (bug #108404)
   7151        if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
   7152            this.keyupReliable = false;
   7153
   7154        // We cannot rely on receiving keyup for Caps Lock on certain platforms
   7155        else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable)
   7156            this.keyupReliable = false;
   7157
   7158        // Determine whether default action for Alt+combinations must be prevented
   7159        var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;
   7160
   7161        // Determine whether default action for Ctrl+combinations must be prevented
   7162        var prevent_ctrl = !this.modifiers.alt;
   7163
   7164        // We must rely on the (potentially buggy) keyIdentifier if preventing
   7165        // the default action is important
   7166        if ((prevent_ctrl && this.modifiers.ctrl)
   7167         || (prevent_alt  && this.modifiers.alt)
   7168         || this.modifiers.meta
   7169         || this.modifiers.hyper)
   7170            this.reliable = true;
   7171
   7172        // Record most recently known keysym by associated key code
   7173        recentKeysym[this.keyCode] = this.keysym;
   7174
   7175    };
   7176
   7177    KeydownEvent.prototype = new KeyEvent();
   7178
   7179    /**
   7180     * Information related to the pressing of a key, which MUST be
   7181     * associated with a printable character. The presence or absence of any
   7182     * information within this object is browser-dependent.
   7183     *
   7184     * @private
   7185     * @constructor
   7186     * @augments Guacamole.Keyboard.KeyEvent
   7187     * @param {!KeyboardEvent} orig
   7188     *     The relevant DOM "keypress" event.
   7189     */
   7190    var KeypressEvent = function KeypressEvent(orig) {
   7191
   7192        // We extend KeyEvent
   7193        KeyEvent.call(this, orig);
   7194
   7195        // Pull keysym from char code
   7196        this.keysym = keysym_from_charcode(this.keyCode);
   7197
   7198        // Keypress is always reliable
   7199        this.reliable = true;
   7200
   7201    };
   7202
   7203    KeypressEvent.prototype = new KeyEvent();
   7204
   7205    /**
   7206     * Information related to the releasing of a key, which need not be a key
   7207     * associated with a printable character. The presence or absence of any
   7208     * information within this object is browser-dependent.
   7209     *
   7210     * @private
   7211     * @constructor
   7212     * @augments Guacamole.Keyboard.KeyEvent
   7213     * @param {!KeyboardEvent} orig
   7214     *     The relevant DOM "keyup" event.
   7215     */
   7216    var KeyupEvent = function KeyupEvent(orig) {
   7217
   7218        // We extend KeyEvent
   7219        KeyEvent.call(this, orig);
   7220
   7221        // If key is known from keyCode or DOM3 alone, use that (keyCode is
   7222        // still more reliable for keyup when dead keys are in use)
   7223        this.keysym =  keysym_from_keycode(this.keyCode, this.location)
   7224                    || keysym_from_key_identifier(this.key, this.location);
   7225
   7226        // Fall back to the most recently pressed keysym associated with the
   7227        // keyCode if the inferred key doesn't seem to actually be pressed
   7228        if (!guac_keyboard.pressed[this.keysym])
   7229            this.keysym = recentKeysym[this.keyCode] || this.keysym;
   7230
   7231        // Keyup is as reliable as it will ever be
   7232        this.reliable = true;
   7233
   7234    };
   7235
   7236    KeyupEvent.prototype = new KeyEvent();
   7237
   7238    /**
   7239     * An array of recorded events, which can be instances of the private
   7240     * KeydownEvent, KeypressEvent, and KeyupEvent classes.
   7241     *
   7242     * @private
   7243     * @type {!KeyEvent[]}
   7244     */
   7245    var eventLog = [];
   7246
   7247    /**
   7248     * Map of known JavaScript keycodes which do not map to typable characters
   7249     * to their X11 keysym equivalents.
   7250     *
   7251     * @private
   7252     * @type {!Object.<number, number[]>}
   7253     */
   7254    var keycodeKeysyms = {
   7255        8:   [0xFF08], // backspace
   7256        9:   [0xFF09], // tab
   7257        12:  [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear       / KP 5
   7258        13:  [0xFF0D], // enter
   7259        16:  [0xFFE1, 0xFFE1, 0xFFE2], // shift
   7260        17:  [0xFFE3, 0xFFE3, 0xFFE4], // ctrl
   7261        18:  [0xFFE9, 0xFFE9, 0xFE03], // alt
   7262        19:  [0xFF13], // pause/break
   7263        20:  [0xFFE5], // caps lock
   7264        27:  [0xFF1B], // escape
   7265        32:  [0x0020], // space
   7266        33:  [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up     / KP 9
   7267        34:  [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down   / KP 3
   7268        35:  [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end         / KP 1
   7269        36:  [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home        / KP 7
   7270        37:  [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow  / KP 4
   7271        38:  [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow    / KP 8
   7272        39:  [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
   7273        40:  [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow  / KP 2
   7274        45:  [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert      / KP 0
   7275        46:  [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete      / KP decimal
   7276        91:  [0xFFE7], // left windows/command key (meta_l)
   7277        92:  [0xFFE8], // right window/command key (meta_r)
   7278        93:  [0xFF67], // menu key
   7279        96:  [0xFFB0], // KP 0
   7280        97:  [0xFFB1], // KP 1
   7281        98:  [0xFFB2], // KP 2
   7282        99:  [0xFFB3], // KP 3
   7283        100: [0xFFB4], // KP 4
   7284        101: [0xFFB5], // KP 5
   7285        102: [0xFFB6], // KP 6
   7286        103: [0xFFB7], // KP 7
   7287        104: [0xFFB8], // KP 8
   7288        105: [0xFFB9], // KP 9
   7289        106: [0xFFAA], // KP multiply
   7290        107: [0xFFAB], // KP add
   7291        109: [0xFFAD], // KP subtract
   7292        110: [0xFFAE], // KP decimal
   7293        111: [0xFFAF], // KP divide
   7294        112: [0xFFBE], // f1
   7295        113: [0xFFBF], // f2
   7296        114: [0xFFC0], // f3
   7297        115: [0xFFC1], // f4
   7298        116: [0xFFC2], // f5
   7299        117: [0xFFC3], // f6
   7300        118: [0xFFC4], // f7
   7301        119: [0xFFC5], // f8
   7302        120: [0xFFC6], // f9
   7303        121: [0xFFC7], // f10
   7304        122: [0xFFC8], // f11
   7305        123: [0xFFC9], // f12
   7306        144: [0xFF7F], // num lock
   7307        145: [0xFF14], // scroll lock
   7308        225: [0xFE03]  // altgraph (iso_level3_shift)
   7309    };
   7310
   7311    /**
   7312     * Map of known JavaScript keyidentifiers which do not map to typable
   7313     * characters to their unshifted X11 keysym equivalents.
   7314     *
   7315     * @private
   7316     * @type {!Object.<string, number[]>}
   7317     */
   7318    var keyidentifier_keysym = {
   7319        "Again": [0xFF66],
   7320        "AllCandidates": [0xFF3D],
   7321        "Alphanumeric": [0xFF30],
   7322        "Alt": [0xFFE9, 0xFFE9, 0xFE03],
   7323        "Attn": [0xFD0E],
   7324        "AltGraph": [0xFE03],
   7325        "ArrowDown": [0xFF54],
   7326        "ArrowLeft": [0xFF51],
   7327        "ArrowRight": [0xFF53],
   7328        "ArrowUp": [0xFF52],
   7329        "Backspace": [0xFF08],
   7330        "CapsLock": [0xFFE5],
   7331        "Cancel": [0xFF69],
   7332        "Clear": [0xFF0B],
   7333        "Convert": [0xFF21],
   7334        "Copy": [0xFD15],
   7335        "Crsel": [0xFD1C],
   7336        "CrSel": [0xFD1C],
   7337        "CodeInput": [0xFF37],
   7338        "Compose": [0xFF20],
   7339        "Control": [0xFFE3, 0xFFE3, 0xFFE4],
   7340        "ContextMenu": [0xFF67],
   7341        "Delete": [0xFFFF],
   7342        "Down": [0xFF54],
   7343        "End": [0xFF57],
   7344        "Enter": [0xFF0D],
   7345        "EraseEof": [0xFD06],
   7346        "Escape": [0xFF1B],
   7347        "Execute": [0xFF62],
   7348        "Exsel": [0xFD1D],
   7349        "ExSel": [0xFD1D],
   7350        "F1": [0xFFBE],
   7351        "F2": [0xFFBF],
   7352        "F3": [0xFFC0],
   7353        "F4": [0xFFC1],
   7354        "F5": [0xFFC2],
   7355        "F6": [0xFFC3],
   7356        "F7": [0xFFC4],
   7357        "F8": [0xFFC5],
   7358        "F9": [0xFFC6],
   7359        "F10": [0xFFC7],
   7360        "F11": [0xFFC8],
   7361        "F12": [0xFFC9],
   7362        "F13": [0xFFCA],
   7363        "F14": [0xFFCB],
   7364        "F15": [0xFFCC],
   7365        "F16": [0xFFCD],
   7366        "F17": [0xFFCE],
   7367        "F18": [0xFFCF],
   7368        "F19": [0xFFD0],
   7369        "F20": [0xFFD1],
   7370        "F21": [0xFFD2],
   7371        "F22": [0xFFD3],
   7372        "F23": [0xFFD4],
   7373        "F24": [0xFFD5],
   7374        "Find": [0xFF68],
   7375        "GroupFirst": [0xFE0C],
   7376        "GroupLast": [0xFE0E],
   7377        "GroupNext": [0xFE08],
   7378        "GroupPrevious": [0xFE0A],
   7379        "FullWidth": null,
   7380        "HalfWidth": null,
   7381        "HangulMode": [0xFF31],
   7382        "Hankaku": [0xFF29],
   7383        "HanjaMode": [0xFF34],
   7384        "Help": [0xFF6A],
   7385        "Hiragana": [0xFF25],
   7386        "HiraganaKatakana": [0xFF27],
   7387        "Home": [0xFF50],
   7388        "Hyper": [0xFFED, 0xFFED, 0xFFEE],
   7389        "Insert": [0xFF63],
   7390        "JapaneseHiragana": [0xFF25],
   7391        "JapaneseKatakana": [0xFF26],
   7392        "JapaneseRomaji": [0xFF24],
   7393        "JunjaMode": [0xFF38],
   7394        "KanaMode": [0xFF2D],
   7395        "KanjiMode": [0xFF21],
   7396        "Katakana": [0xFF26],
   7397        "Left": [0xFF51],
   7398        "Meta": [0xFFE7, 0xFFE7, 0xFFE8],
   7399        "ModeChange": [0xFF7E],
   7400        "NumLock": [0xFF7F],
   7401        "PageDown": [0xFF56],
   7402        "PageUp": [0xFF55],
   7403        "Pause": [0xFF13],
   7404        "Play": [0xFD16],
   7405        "PreviousCandidate": [0xFF3E],
   7406        "PrintScreen": [0xFF61],
   7407        "Redo": [0xFF66],
   7408        "Right": [0xFF53],
   7409        "RomanCharacters": null,
   7410        "Scroll": [0xFF14],
   7411        "Select": [0xFF60],
   7412        "Separator": [0xFFAC],
   7413        "Shift": [0xFFE1, 0xFFE1, 0xFFE2],
   7414        "SingleCandidate": [0xFF3C],
   7415        "Super": [0xFFEB, 0xFFEB, 0xFFEC],
   7416        "Tab": [0xFF09],
   7417        "UIKeyInputDownArrow": [0xFF54],
   7418        "UIKeyInputEscape": [0xFF1B],
   7419        "UIKeyInputLeftArrow": [0xFF51],
   7420        "UIKeyInputRightArrow": [0xFF53],
   7421        "UIKeyInputUpArrow": [0xFF52],
   7422        "Up": [0xFF52],
   7423        "Undo": [0xFF65],
   7424        "Win": [0xFFE7, 0xFFE7, 0xFFE8],
   7425        "Zenkaku": [0xFF28],
   7426        "ZenkakuHankaku": [0xFF2A]
   7427    };
   7428
   7429    /**
   7430     * All keysyms which should not repeat when held down.
   7431     *
   7432     * @private
   7433     * @type {!Object.<number, boolean>}
   7434     */
   7435    var no_repeat = {
   7436        0xFE03: true, // ISO Level 3 Shift (AltGr)
   7437        0xFFE1: true, // Left shift
   7438        0xFFE2: true, // Right shift
   7439        0xFFE3: true, // Left ctrl 
   7440        0xFFE4: true, // Right ctrl 
   7441        0xFFE5: true, // Caps Lock
   7442        0xFFE7: true, // Left meta 
   7443        0xFFE8: true, // Right meta 
   7444        0xFFE9: true, // Left alt
   7445        0xFFEA: true, // Right alt
   7446        0xFFEB: true, // Left super/hyper
   7447        0xFFEC: true  // Right super/hyper
   7448    };
   7449
   7450    /**
   7451     * All modifiers and their states.
   7452     *
   7453     * @type {!Guacamole.Keyboard.ModifierState}
   7454     */
   7455    this.modifiers = new Guacamole.Keyboard.ModifierState();
   7456        
   7457    /**
   7458     * The state of every key, indexed by keysym. If a particular key is
   7459     * pressed, the value of pressed for that keysym will be true. If a key
   7460     * is not currently pressed, it will not be defined. 
   7461     *
   7462     * @type {!Object.<number, boolean>}
   7463     */
   7464    this.pressed = {};
   7465
   7466    /**
   7467     * The state of every key, indexed by keysym, for strictly those keys whose
   7468     * status has been indirectly determined thorugh observation of other key
   7469     * events. If a particular key is implicitly pressed, the value of
   7470     * implicitlyPressed for that keysym will be true. If a key
   7471     * is not currently implicitly pressed (the key is not pressed OR the state
   7472     * of the key is explicitly known), it will not be defined.
   7473     *
   7474     * @private
   7475     * @type {!Object.<number, boolean>}
   7476     */
   7477    var implicitlyPressed = {};
   7478
   7479    /**
   7480     * The last result of calling the onkeydown handler for each key, indexed
   7481     * by keysym. This is used to prevent/allow default actions for key events,
   7482     * even when the onkeydown handler cannot be called again because the key
   7483     * is (theoretically) still pressed.
   7484     *
   7485     * @private
   7486     * @type {!Object.<number, boolean>}
   7487     */
   7488    var last_keydown_result = {};
   7489
   7490    /**
   7491     * The keysym most recently associated with a given keycode when keydown
   7492     * fired. This object maps keycodes to keysyms.
   7493     *
   7494     * @private
   7495     * @type {!Object.<number, number>}
   7496     */
   7497    var recentKeysym = {};
   7498
   7499    /**
   7500     * Timeout before key repeat starts.
   7501     *
   7502     * @private
   7503     * @type {number}
   7504     */
   7505    var key_repeat_timeout = null;
   7506
   7507    /**
   7508     * Interval which presses and releases the last key pressed while that
   7509     * key is still being held down.
   7510     *
   7511     * @private
   7512     * @type {number}
   7513     */
   7514    var key_repeat_interval = null;
   7515
   7516    /**
   7517     * Given an array of keysyms indexed by location, returns the keysym
   7518     * for the given location, or the keysym for the standard location if
   7519     * undefined.
   7520     * 
   7521     * @private
   7522     * @param {number[]} keysyms
   7523     *     An array of keysyms, where the index of the keysym in the array is
   7524     *     the location value.
   7525     *
   7526     * @param {!number} location
   7527     *     The location on the keyboard corresponding to the key pressed, as
   7528     *     defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
   7529     */
   7530    var get_keysym = function get_keysym(keysyms, location) {
   7531
   7532        if (!keysyms)
   7533            return null;
   7534
   7535        return keysyms[location] || keysyms[0];
   7536    };
   7537
   7538    /**
   7539     * Returns true if the given keysym corresponds to a printable character,
   7540     * false otherwise.
   7541     *
   7542     * @param {!number} keysym
   7543     *     The keysym to check.
   7544     *
   7545     * @returns {!boolean}
   7546     *     true if the given keysym corresponds to a printable character,
   7547     *     false otherwise.
   7548     */
   7549    var isPrintable = function isPrintable(keysym) {
   7550
   7551        // Keysyms with Unicode equivalents are printable
   7552        return (keysym >= 0x00 && keysym <= 0xFF)
   7553            || (keysym & 0xFFFF0000) === 0x01000000;
   7554
   7555    };
   7556
   7557    function keysym_from_key_identifier(identifier, location, shifted) {
   7558
   7559        if (!identifier)
   7560            return null;
   7561
   7562        var typedCharacter;
   7563
   7564        // If identifier is U+xxxx, decode Unicode character 
   7565        var unicodePrefixLocation = identifier.indexOf("U+");
   7566        if (unicodePrefixLocation >= 0) {
   7567            var hex = identifier.substring(unicodePrefixLocation+2);
   7568            typedCharacter = String.fromCharCode(parseInt(hex, 16));
   7569        }
   7570
   7571        // If single character and not keypad, use that as typed character
   7572        else if (identifier.length === 1 && location !== 3)
   7573            typedCharacter = identifier;
   7574
   7575        // Otherwise, look up corresponding keysym
   7576        else
   7577            return get_keysym(keyidentifier_keysym[identifier], location);
   7578
   7579        // Alter case if necessary
   7580        if (shifted === true)
   7581            typedCharacter = typedCharacter.toUpperCase();
   7582        else if (shifted === false)
   7583            typedCharacter = typedCharacter.toLowerCase();
   7584
   7585        // Get codepoint
   7586        var codepoint = typedCharacter.charCodeAt(0);
   7587        return keysym_from_charcode(codepoint);
   7588
   7589    }
   7590
   7591    function isControlCharacter(codepoint) {
   7592        return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
   7593    }
   7594
   7595    function keysym_from_charcode(codepoint) {
   7596
   7597        // Keysyms for control characters
   7598        if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
   7599
   7600        // Keysyms for ASCII chars
   7601        if (codepoint >= 0x0000 && codepoint <= 0x00FF)
   7602            return codepoint;
   7603
   7604        // Keysyms for Unicode
   7605        if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
   7606            return 0x01000000 | codepoint;
   7607
   7608        return null;
   7609
   7610    }
   7611
   7612    function keysym_from_keycode(keyCode, location) {
   7613        return get_keysym(keycodeKeysyms[keyCode], location);
   7614    }
   7615
   7616    /**
   7617     * Heuristically detects if the legacy keyIdentifier property of
   7618     * a keydown/keyup event looks incorrectly derived. Chrome, and
   7619     * presumably others, will produce the keyIdentifier by assuming
   7620     * the keyCode is the Unicode codepoint for that key. This is not
   7621     * correct in all cases.
   7622     *
   7623     * @private
   7624     * @param {!number} keyCode
   7625     *     The keyCode from a browser keydown/keyup event.
   7626     *
   7627     * @param {string} keyIdentifier
   7628     *     The legacy keyIdentifier from a browser keydown/keyup event.
   7629     *
   7630     * @returns {!boolean}
   7631     *     true if the keyIdentifier looks sane, false if the keyIdentifier
   7632     *     appears incorrectly derived or is missing entirely.
   7633     */
   7634    var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {
   7635
   7636        // Missing identifier is not sane
   7637        if (!keyIdentifier)
   7638            return false;
   7639
   7640        // Assume non-Unicode keyIdentifier values are sane
   7641        var unicodePrefixLocation = keyIdentifier.indexOf("U+");
   7642        if (unicodePrefixLocation === -1)
   7643            return true;
   7644
   7645        // If the Unicode codepoint isn't identical to the keyCode,
   7646        // then the identifier is likely correct
   7647        var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
   7648        if (keyCode !== codepoint)
   7649            return true;
   7650
   7651        // The keyCodes for A-Z and 0-9 are actually identical to their
   7652        // Unicode codepoints
   7653        if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
   7654            return true;
   7655
   7656        // The keyIdentifier does NOT appear sane
   7657        return false;
   7658
   7659    };
   7660
   7661    /**
   7662     * Marks a key as pressed, firing the keydown event if registered. Key
   7663     * repeat for the pressed key will start after a delay if that key is
   7664     * not a modifier. The return value of this function depends on the
   7665     * return value of the keydown event handler, if any.
   7666     * 
   7667     * @param {number} keysym
   7668     *     The keysym of the key to press.
   7669     *
   7670     * @return {boolean}
   7671     *     true if event should NOT be canceled, false otherwise.
   7672     */
   7673    this.press = function(keysym) {
   7674
   7675        // Don't bother with pressing the key if the key is unknown
   7676        if (keysym === null) return;
   7677
   7678        // Only press if released
   7679        if (!guac_keyboard.pressed[keysym]) {
   7680
   7681            // Mark key as pressed
   7682            guac_keyboard.pressed[keysym] = true;
   7683
   7684            // Send key event
   7685            if (guac_keyboard.onkeydown) {
   7686                var result = guac_keyboard.onkeydown(keysym);
   7687                last_keydown_result[keysym] = result;
   7688
   7689                // Stop any current repeat
   7690                window.clearTimeout(key_repeat_timeout);
   7691                window.clearInterval(key_repeat_interval);
   7692
   7693                // Repeat after a delay as long as pressed
   7694                if (!no_repeat[keysym])
   7695                    key_repeat_timeout = window.setTimeout(function() {
   7696                        key_repeat_interval = window.setInterval(function() {
   7697                            guac_keyboard.onkeyup(keysym);
   7698                            guac_keyboard.onkeydown(keysym);
   7699                        }, 50);
   7700                    }, 500);
   7701
   7702                return result;
   7703            }
   7704        }
   7705
   7706        // Return the last keydown result by default, resort to false if unknown
   7707        return last_keydown_result[keysym] || false;
   7708
   7709    };
   7710
   7711    /**
   7712     * Marks a key as released, firing the keyup event if registered.
   7713     * 
   7714     * @param {number} keysym
   7715     *     The keysym of the key to release.
   7716     */
   7717    this.release = function(keysym) {
   7718
   7719        // Only release if pressed
   7720        if (guac_keyboard.pressed[keysym]) {
   7721            
   7722            // Mark key as released
   7723            delete guac_keyboard.pressed[keysym];
   7724            delete implicitlyPressed[keysym];
   7725
   7726            // Stop repeat
   7727            window.clearTimeout(key_repeat_timeout);
   7728            window.clearInterval(key_repeat_interval);
   7729
   7730            // Send key event
   7731            if (keysym !== null && guac_keyboard.onkeyup)
   7732                guac_keyboard.onkeyup(keysym);
   7733
   7734        }
   7735
   7736    };
   7737
   7738    /**
   7739     * Presses and releases the keys necessary to type the given string of
   7740     * text.
   7741     *
   7742     * @param {!string} str
   7743     *     The string to type.
   7744     */
   7745    this.type = function type(str) {
   7746
   7747        // Press/release the key corresponding to each character in the string
   7748        for (var i = 0; i < str.length; i++) {
   7749
   7750            // Determine keysym of current character
   7751            var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i);
   7752            var keysym = keysym_from_charcode(codepoint);
   7753
   7754            // Press and release key for current character
   7755            guac_keyboard.press(keysym);
   7756            guac_keyboard.release(keysym);
   7757
   7758        }
   7759
   7760    };
   7761
   7762    /**
   7763     * Resets the state of this keyboard, releasing all keys, and firing keyup
   7764     * events for each released key.
   7765     */
   7766    this.reset = function() {
   7767
   7768        // Release all pressed keys
   7769        for (var keysym in guac_keyboard.pressed)
   7770            guac_keyboard.release(parseInt(keysym));
   7771
   7772        // Clear event log
   7773        eventLog = [];
   7774
   7775    };
   7776
   7777    /**
   7778     * Resynchronizes the remote state of the given modifier with its
   7779     * corresponding local modifier state, as dictated by
   7780     * {@link KeyEvent#modifiers} within the given key event, by pressing or
   7781     * releasing keysyms.
   7782     *
   7783     * @private
   7784     * @param {!string} modifier
   7785     *     The name of the {@link Guacamole.Keyboard.ModifierState} property
   7786     *     being updated.
   7787     *
   7788     * @param {!number[]} keysyms
   7789     *     The keysyms which represent the modifier being updated.
   7790     *
   7791     * @param {!KeyEvent} keyEvent
   7792     *     Guacamole's current best interpretation of the key event being
   7793     *     processed.
   7794     */
   7795    var updateModifierState = function updateModifierState(modifier,
   7796        keysyms, keyEvent) {
   7797
   7798        var localState = keyEvent.modifiers[modifier];
   7799        var remoteState = guac_keyboard.modifiers[modifier];
   7800
   7801        var i;
   7802
   7803        // Do not trust changes in modifier state for events directly involving
   7804        // that modifier: (1) the flag may erroneously be cleared despite
   7805        // another version of the same key still being held and (2) the change
   7806        // in flag may be due to the current event being processed, thus
   7807        // updating things here is at best redundant and at worst incorrect
   7808        if (keysyms.indexOf(keyEvent.keysym) !== -1)
   7809            return;
   7810
   7811        // Release all related keys if modifier is implicitly released
   7812        if (remoteState && localState === false) {
   7813            for (i = 0; i < keysyms.length; i++) {
   7814                guac_keyboard.release(keysyms[i]);
   7815            }
   7816        }
   7817
   7818        // Press if modifier is implicitly pressed
   7819        else if (!remoteState && localState) {
   7820
   7821            // Verify that modifier flag isn't already pressed or already set
   7822            // due to another version of the same key being held down
   7823            for (i = 0; i < keysyms.length; i++) {
   7824                if (guac_keyboard.pressed[keysyms[i]])
   7825                    return;
   7826            }
   7827
   7828            // Mark as implicitly pressed only if there is other information
   7829            // within the key event relating to a different key. Some
   7830            // platforms, such as iOS, will send essentially empty key events
   7831            // for modifier keys, using only the modifier flags to signal the
   7832            // identity of the key.
   7833            var keysym = keysyms[0];
   7834            if (keyEvent.keysym)
   7835                implicitlyPressed[keysym] = true;
   7836
   7837            guac_keyboard.press(keysym);
   7838
   7839        }
   7840
   7841    };
   7842
   7843    /**
   7844     * Given a keyboard event, updates the remote key state to match the local
   7845     * modifier state and remote based on the modifier flags within the event.
   7846     * This function pays no attention to keycodes.
   7847     *
   7848     * @private
   7849     * @param {!KeyEvent} keyEvent
   7850     *     Guacamole's current best interpretation of the key event being
   7851     *     processed.
   7852     */
   7853    var syncModifierStates = function syncModifierStates(keyEvent) {
   7854
   7855        // Resync state of alt
   7856        updateModifierState('alt', [
   7857            0xFFE9, // Left alt
   7858            0xFFEA, // Right alt
   7859            0xFE03  // AltGr
   7860        ], keyEvent);
   7861
   7862        // Resync state of shift
   7863        updateModifierState('shift', [
   7864            0xFFE1, // Left shift
   7865            0xFFE2  // Right shift
   7866        ], keyEvent);
   7867
   7868        // Resync state of ctrl
   7869        updateModifierState('ctrl', [
   7870            0xFFE3, // Left ctrl
   7871            0xFFE4  // Right ctrl
   7872        ], keyEvent);
   7873
   7874        // Resync state of meta
   7875        updateModifierState('meta', [
   7876            0xFFE7, // Left meta
   7877            0xFFE8  // Right meta
   7878        ], keyEvent);
   7879
   7880        // Resync state of hyper
   7881        updateModifierState('hyper', [
   7882            0xFFEB, // Left super/hyper
   7883            0xFFEC  // Right super/hyper
   7884        ], keyEvent);
   7885
   7886        // Update state
   7887        guac_keyboard.modifiers = keyEvent.modifiers;
   7888
   7889    };
   7890
   7891    /**
   7892     * Returns whether all currently pressed keys were implicitly pressed. A
   7893     * key is implicitly pressed if its status was inferred indirectly from
   7894     * inspection of other key events.
   7895     *
   7896     * @private
   7897     * @returns {!boolean}
   7898     *     true if all currently pressed keys were implicitly pressed, false
   7899     *     otherwise.
   7900     */
   7901    var isStateImplicit = function isStateImplicit() {
   7902
   7903        for (var keysym in guac_keyboard.pressed) {
   7904            if (!implicitlyPressed[keysym])
   7905                return false;
   7906        }
   7907
   7908        return true;
   7909
   7910    };
   7911
   7912    /**
   7913     * Reads through the event log, removing events from the head of the log
   7914     * when the corresponding true key presses are known (or as known as they
   7915     * can be).
   7916     * 
   7917     * @private
   7918     * @return {boolean}
   7919     *     Whether the default action of the latest event should be prevented.
   7920     */
   7921    function interpret_events() {
   7922
   7923        // Do not prevent default if no event could be interpreted
   7924        var handled_event = interpret_event();
   7925        if (!handled_event)
   7926            return false;
   7927
   7928        // Interpret as much as possible
   7929        var last_event;
   7930        do {
   7931            last_event = handled_event;
   7932            handled_event = interpret_event();
   7933        } while (handled_event !== null);
   7934
   7935        // Reset keyboard state if we cannot expect to receive any further
   7936        // keyup events
   7937        if (isStateImplicit())
   7938            guac_keyboard.reset();
   7939
   7940        return last_event.defaultPrevented;
   7941
   7942    }
   7943
   7944    /**
   7945     * Releases Ctrl+Alt, if both are currently pressed and the given keysym
   7946     * looks like a key that may require AltGr.
   7947     *
   7948     * @private
   7949     * @param {!number} keysym
   7950     *     The key that was just pressed.
   7951     */
   7952    var release_simulated_altgr = function release_simulated_altgr(keysym) {
   7953
   7954        // Both Ctrl+Alt must be pressed if simulated AltGr is in use
   7955        if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt)
   7956            return;
   7957
   7958        // Assume [A-Z] never require AltGr
   7959        if (keysym >= 0x0041 && keysym <= 0x005A)
   7960            return;
   7961
   7962        // Assume [a-z] never require AltGr
   7963        if (keysym >= 0x0061 && keysym <= 0x007A)
   7964            return;
   7965
   7966        // Release Ctrl+Alt if the keysym is printable
   7967        if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) {
   7968            guac_keyboard.release(0xFFE3); // Left ctrl 
   7969            guac_keyboard.release(0xFFE4); // Right ctrl 
   7970            guac_keyboard.release(0xFFE9); // Left alt
   7971            guac_keyboard.release(0xFFEA); // Right alt
   7972        }
   7973
   7974    };
   7975
   7976    /**
   7977     * Reads through the event log, interpreting the first event, if possible,
   7978     * and returning that event. If no events can be interpreted, due to a
   7979     * total lack of events or the need for more events, null is returned. Any
   7980     * interpreted events are automatically removed from the log.
   7981     * 
   7982     * @private
   7983     * @return {KeyEvent}
   7984     *     The first key event in the log, if it can be interpreted, or null
   7985     *     otherwise.
   7986     */
   7987    var interpret_event = function interpret_event() {
   7988
   7989        // Peek at first event in log
   7990        var first = eventLog[0];
   7991        if (!first)
   7992            return null;
   7993
   7994        // Keydown event
   7995        if (first instanceof KeydownEvent) {
   7996
   7997            var keysym = null;
   7998            var accepted_events = [];
   7999
   8000            // Defer handling of Meta until it is known to be functioning as a
   8001            // modifier (it may otherwise actually be an alternative method for
   8002            // pressing a single key, such as Meta+Left for Home on ChromeOS)
   8003            if (first.keysym === 0xFFE7 || first.keysym === 0xFFE8) {
   8004
   8005                // Defer handling until further events exist to provide context
   8006                if (eventLog.length === 1)
   8007                    return null;
   8008
   8009                // Drop keydown if it turns out Meta does not actually apply
   8010                if (eventLog[1].keysym !== first.keysym) {
   8011                    if (!eventLog[1].modifiers.meta)
   8012                        return eventLog.shift();
   8013                }
   8014
   8015                // Drop duplicate keydown events while waiting to determine
   8016                // whether to acknowledge Meta (browser may repeat keydown
   8017                // while the key is held)
   8018                else if (eventLog[1] instanceof KeydownEvent)
   8019                    return eventLog.shift();
   8020
   8021            }
   8022
   8023            // If event itself is reliable, no need to wait for other events
   8024            if (first.reliable) {
   8025                keysym = first.keysym;
   8026                accepted_events = eventLog.splice(0, 1);
   8027            }
   8028
   8029            // If keydown is immediately followed by a keypress, use the indicated character
   8030            else if (eventLog[1] instanceof KeypressEvent) {
   8031                keysym = eventLog[1].keysym;
   8032                accepted_events = eventLog.splice(0, 2);
   8033            }
   8034
   8035            // If keydown is immediately followed by anything else, then no
   8036            // keypress can possibly occur to clarify this event, and we must
   8037            // handle it now
   8038            else if (eventLog[1]) {
   8039                keysym = first.keysym;
   8040                accepted_events = eventLog.splice(0, 1);
   8041            }
   8042
   8043            // Fire a key press if valid events were found
   8044            if (accepted_events.length > 0) {
   8045
   8046                syncModifierStates(first);
   8047
   8048                if (keysym) {
   8049
   8050                    // Fire event
   8051                    release_simulated_altgr(keysym);
   8052                    var defaultPrevented = !guac_keyboard.press(keysym);
   8053                    recentKeysym[first.keyCode] = keysym;
   8054
   8055                    // Release the key now if we cannot rely on the associated
   8056                    // keyup event
   8057                    if (!first.keyupReliable)
   8058                        guac_keyboard.release(keysym);
   8059
   8060                    // Record whether default was prevented
   8061                    for (var i=0; i<accepted_events.length; i++)
   8062                        accepted_events[i].defaultPrevented = defaultPrevented;
   8063
   8064                }
   8065
   8066                return first;
   8067
   8068            }
   8069
   8070        } // end if keydown
   8071
   8072        // Keyup event
   8073        else if (first instanceof KeyupEvent && !quirks.keyupUnreliable) {
   8074
   8075            // Release specific key if known
   8076            var keysym = first.keysym;
   8077            if (keysym) {
   8078                guac_keyboard.release(keysym);
   8079                delete recentKeysym[first.keyCode];
   8080                first.defaultPrevented = true;
   8081            }
   8082
   8083            // Otherwise, fall back to releasing all keys
   8084            else {
   8085                guac_keyboard.reset();
   8086                return first;
   8087            }
   8088
   8089            syncModifierStates(first);
   8090            return eventLog.shift();
   8091
   8092        } // end if keyup
   8093
   8094        // Ignore any other type of event (keypress by itself is invalid, and
   8095        // unreliable keyup events should simply be dumped)
   8096        else
   8097            return eventLog.shift();
   8098
   8099        // No event interpreted
   8100        return null;
   8101
   8102    };
   8103
   8104    /**
   8105     * Returns the keyboard location of the key associated with the given
   8106     * keyboard event. The location differentiates key events which otherwise
   8107     * have the same keycode, such as left shift vs. right shift.
   8108     *
   8109     * @private
   8110     * @param {!KeyboardEvent} e
   8111     *     A JavaScript keyboard event, as received through the DOM via a
   8112     *     "keydown", "keyup", or "keypress" handler.
   8113     *
   8114     * @returns {!number}
   8115     *     The location of the key event on the keyboard, as defined at:
   8116     *     http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
   8117     */
   8118    var getEventLocation = function getEventLocation(e) {
   8119
   8120        // Use standard location, if possible
   8121        if ('location' in e)
   8122            return e.location;
   8123
   8124        // Failing that, attempt to use deprecated keyLocation
   8125        if ('keyLocation' in e)
   8126            return e.keyLocation;
   8127
   8128        // If no location is available, assume left side
   8129        return 0;
   8130
   8131    };
   8132
   8133    /**
   8134     * Attempts to mark the given Event as having been handled by this
   8135     * Guacamole.Keyboard. If the Event has already been marked as handled,
   8136     * false is returned.
   8137     *
   8138     * @param {!Event} e
   8139     *     The Event to mark.
   8140     *
   8141     * @returns {!boolean}
   8142     *     true if the given Event was successfully marked, false if the given
   8143     *     Event was already marked.
   8144     */
   8145    var markEvent = function markEvent(e) {
   8146
   8147        // Fail if event is already marked
   8148        if (e[EVENT_MARKER])
   8149            return false;
   8150
   8151        // Mark event otherwise
   8152        e[EVENT_MARKER] = true;
   8153        return true;
   8154
   8155    };
   8156
   8157    /**
   8158     * Attaches event listeners to the given Element, automatically translating
   8159     * received key, input, and composition events into simple keydown/keyup
   8160     * events signalled through this Guacamole.Keyboard's onkeydown and
   8161     * onkeyup handlers.
   8162     *
   8163     * @param {!(Element|Document)} element
   8164     *     The Element to attach event listeners to for the sake of handling
   8165     *     key or input events.
   8166     */
   8167    this.listenTo = function listenTo(element) {
   8168
   8169        // When key pressed
   8170        element.addEventListener("keydown", function(e) {
   8171
   8172            // Only intercept if handler set
   8173            if (!guac_keyboard.onkeydown) return;
   8174
   8175            // Ignore events which have already been handled
   8176            if (!markEvent(e)) return;
   8177
   8178            var keydownEvent = new KeydownEvent(e);
   8179
   8180            // Ignore (but do not prevent) the "composition" keycode sent by some
   8181            // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html)
   8182            if (keydownEvent.keyCode === 229)
   8183                return;
   8184
   8185            // Log event
   8186            eventLog.push(keydownEvent);
   8187
   8188            // Interpret as many events as possible, prevent default if indicated
   8189            if (interpret_events())
   8190                e.preventDefault();
   8191
   8192        }, true);
   8193
   8194        // When key pressed
   8195        element.addEventListener("keypress", function(e) {
   8196
   8197            // Only intercept if handler set
   8198            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
   8199
   8200            // Ignore events which have already been handled
   8201            if (!markEvent(e)) return;
   8202
   8203            // Log event
   8204            eventLog.push(new KeypressEvent(e));
   8205
   8206            // Interpret as many events as possible, prevent default if indicated
   8207            if (interpret_events())
   8208                e.preventDefault();
   8209
   8210        }, true);
   8211
   8212        // When key released
   8213        element.addEventListener("keyup", function(e) {
   8214
   8215            // Only intercept if handler set
   8216            if (!guac_keyboard.onkeyup) return;
   8217
   8218            // Ignore events which have already been handled
   8219            if (!markEvent(e)) return;
   8220
   8221            e.preventDefault();
   8222
   8223            // Log event, call for interpretation
   8224            eventLog.push(new KeyupEvent(e));
   8225            interpret_events();
   8226
   8227        }, true);
   8228
   8229        /**
   8230         * Handles the given "input" event, typing the data within the input text.
   8231         * If the event is complete (text is provided), handling of "compositionend"
   8232         * events is suspended, as such events may conflict with input events.
   8233         *
   8234         * @private
   8235         * @param {!InputEvent} e
   8236         *     The "input" event to handle.
   8237         */
   8238        var handleInput = function handleInput(e) {
   8239
   8240            // Only intercept if handler set
   8241            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
   8242
   8243            // Ignore events which have already been handled
   8244            if (!markEvent(e)) return;
   8245
   8246            // Type all content written
   8247            if (e.data && !e.isComposing) {
   8248                element.removeEventListener("compositionend", handleComposition, false);
   8249                guac_keyboard.type(e.data);
   8250            }
   8251
   8252        };
   8253
   8254        /**
   8255         * Handles the given "compositionend" event, typing the data within the
   8256         * composed text. If the event is complete (composed text is provided),
   8257         * handling of "input" events is suspended, as such events may conflict
   8258         * with composition events.
   8259         *
   8260         * @private
   8261         * @param {!CompositionEvent} e
   8262         *     The "compositionend" event to handle.
   8263         */
   8264        var handleComposition = function handleComposition(e) {
   8265
   8266            // Only intercept if handler set
   8267            if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return;
   8268
   8269            // Ignore events which have already been handled
   8270            if (!markEvent(e)) return;
   8271
   8272            // Type all content written
   8273            if (e.data) {
   8274                element.removeEventListener("input", handleInput, false);
   8275                guac_keyboard.type(e.data);
   8276            }
   8277
   8278        };
   8279
   8280        // Automatically type text entered into the wrapped field
   8281        element.addEventListener("input", handleInput, false);
   8282        element.addEventListener("compositionend", handleComposition, false);
   8283
   8284    };
   8285
   8286    // Listen to given element, if any
   8287    if (element)
   8288        guac_keyboard.listenTo(element);
   8289
   8290};
   8291
   8292/**
   8293 * The unique numerical identifier to assign to the next Guacamole.Keyboard
   8294 * instance.
   8295 *
   8296 * @private
   8297 * @type {!number}
   8298 */
   8299Guacamole.Keyboard._nextID = 0;
   8300
   8301/**
   8302 * The state of all supported keyboard modifiers.
   8303 * @constructor
   8304 */
   8305Guacamole.Keyboard.ModifierState = function() {
   8306    
   8307    /**
   8308     * Whether shift is currently pressed.
   8309     *
   8310     * @type {!boolean}
   8311     */
   8312    this.shift = false;
   8313    
   8314    /**
   8315     * Whether ctrl is currently pressed.
   8316     *
   8317     * @type {!boolean}
   8318     */
   8319    this.ctrl = false;
   8320    
   8321    /**
   8322     * Whether alt is currently pressed.
   8323     *
   8324     * @type {!boolean}
   8325     */
   8326    this.alt = false;
   8327    
   8328    /**
   8329     * Whether meta (apple key) is currently pressed.
   8330     *
   8331     * @type {!boolean}
   8332     */
   8333    this.meta = false;
   8334
   8335    /**
   8336     * Whether hyper (windows key) is currently pressed.
   8337     *
   8338     * @type {!boolean}
   8339     */
   8340    this.hyper = false;
   8341
   8342};
   8343
   8344/**
   8345 * Returns the modifier state applicable to the keyboard event given.
   8346 * 
   8347 * @param {!KeyboardEvent} e
   8348 *     The keyboard event to read.
   8349 *
   8350 * @returns {!Guacamole.Keyboard.ModifierState}
   8351 *     The current state of keyboard modifiers.
   8352 */
   8353Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) {
   8354    
   8355    var state = new Guacamole.Keyboard.ModifierState();
   8356
   8357    // Assign states from old flags
   8358    state.shift = e.shiftKey;
   8359    state.ctrl  = e.ctrlKey;
   8360    state.alt   = e.altKey;
   8361    state.meta  = e.metaKey;
   8362
   8363    // Use DOM3 getModifierState() for others
   8364    if (e.getModifierState) {
   8365        state.hyper = e.getModifierState("OS")
   8366                   || e.getModifierState("Super")
   8367                   || e.getModifierState("Hyper")
   8368                   || e.getModifierState("Win");
   8369    }
   8370
   8371    return state;
   8372    
   8373};
   8374/*
   8375 * Licensed to the Apache Software Foundation (ASF) under one
   8376 * or more contributor license agreements.  See the NOTICE file
   8377 * distributed with this work for additional information
   8378 * regarding copyright ownership.  The ASF licenses this file
   8379 * to you under the Apache License, Version 2.0 (the
   8380 * "License"); you may not use this file except in compliance
   8381 * with the License.  You may obtain a copy of the License at
   8382 *
   8383 *   http://www.apache.org/licenses/LICENSE-2.0
   8384 *
   8385 * Unless required by applicable law or agreed to in writing,
   8386 * software distributed under the License is distributed on an
   8387 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   8388 * KIND, either express or implied.  See the License for the
   8389 * specific language governing permissions and limitations
   8390 * under the License.
   8391 */
   8392
   8393var Guacamole = Guacamole || {};
   8394
   8395/**
   8396 * Abstract ordered drawing surface. Each Layer contains a canvas element and
   8397 * provides simple drawing instructions for drawing to that canvas element,
   8398 * however unlike the canvas element itself, drawing operations on a Layer are
   8399 * guaranteed to run in order, even if such an operation must wait for an image
   8400 * to load before completing.
   8401 * 
   8402 * @constructor
   8403 * 
   8404 * @param {!number} width
   8405 *     The width of the Layer, in pixels. The canvas element backing this Layer
   8406 *     will be given this width.
   8407 *                       
   8408 * @param {!number} height
   8409 *     The height of the Layer, in pixels. The canvas element backing this
   8410 *     Layer will be given this height.
   8411 */
   8412Guacamole.Layer = function(width, height) {
   8413
   8414    /**
   8415     * Reference to this Layer.
   8416     *
   8417     * @private
   8418     * @type {!Guacamole.Layer}
   8419     */
   8420    var layer = this;
   8421
   8422    /**
   8423     * The number of pixels the width or height of a layer must change before
   8424     * the underlying canvas is resized. The underlying canvas will be kept at
   8425     * dimensions which are integer multiples of this factor.
   8426     *
   8427     * @private
   8428     * @constant
   8429     * @type {!number}
   8430     */
   8431    var CANVAS_SIZE_FACTOR = 64;
   8432
   8433    /**
   8434     * The canvas element backing this Layer.
   8435     *
   8436     * @private
   8437     * @type {!HTMLCanvasElement}
   8438     */
   8439    var canvas = document.createElement("canvas");
   8440
   8441    /**
   8442     * The 2D display context of the canvas element backing this Layer.
   8443     *
   8444     * @private
   8445     * @type {!CanvasRenderingContext2D}
   8446     */
   8447    var context = canvas.getContext("2d");
   8448    context.save();
   8449
   8450    /**
   8451     * Whether the layer has not yet been drawn to. Once any draw operation
   8452     * which affects the underlying canvas is invoked, this flag will be set to
   8453     * false.
   8454     *
   8455     * @private
   8456     * @type {!boolean}
   8457     */
   8458    var empty = true;
   8459
   8460    /**
   8461     * Whether a new path should be started with the next path drawing
   8462     * operations.
   8463     *
   8464     * @private
   8465     * @type {!boolean}
   8466     */
   8467    var pathClosed = true;
   8468
   8469    /**
   8470     * The number of states on the state stack.
   8471     * 
   8472     * Note that there will ALWAYS be one element on the stack, but that
   8473     * element is not exposed. It is only used to reset the layer to its
   8474     * initial state.
   8475     * 
   8476     * @private
   8477     * @type {!number}
   8478     */
   8479    var stackSize = 0;
   8480
   8481    /**
   8482     * Map of all Guacamole channel masks to HTML5 canvas composite operation
   8483     * names. Not all channel mask combinations are currently implemented.
   8484     *
   8485     * @private
   8486     * @type {!Object.<number, string>}
   8487     */
   8488    var compositeOperation = {
   8489     /* 0x0 NOT IMPLEMENTED */
   8490        0x1: "destination-in",
   8491        0x2: "destination-out",
   8492     /* 0x3 NOT IMPLEMENTED */
   8493        0x4: "source-in",
   8494     /* 0x5 NOT IMPLEMENTED */
   8495        0x6: "source-atop",
   8496     /* 0x7 NOT IMPLEMENTED */
   8497        0x8: "source-out",
   8498        0x9: "destination-atop",
   8499        0xA: "xor",
   8500        0xB: "destination-over",
   8501        0xC: "copy",
   8502     /* 0xD NOT IMPLEMENTED */
   8503        0xE: "source-over",
   8504        0xF: "lighter"
   8505    };
   8506
   8507    /**
   8508     * Resizes the canvas element backing this Layer. This function should only
   8509     * be used internally.
   8510     * 
   8511     * @private
   8512     * @param {number} [newWidth=0]
   8513     *     The new width to assign to this Layer.
   8514     *
   8515     * @param {number} [newHeight=0]
   8516     *     The new height to assign to this Layer.
   8517     */
   8518    var resize = function resize(newWidth, newHeight) {
   8519
   8520        // Default size to zero
   8521        newWidth = newWidth || 0;
   8522        newHeight = newHeight || 0;
   8523
   8524        // Calculate new dimensions of internal canvas
   8525        var canvasWidth  = Math.ceil(newWidth  / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR;
   8526        var canvasHeight = Math.ceil(newHeight / CANVAS_SIZE_FACTOR) * CANVAS_SIZE_FACTOR;
   8527
   8528        // Resize only if canvas dimensions are actually changing
   8529        if (canvas.width !== canvasWidth || canvas.height !== canvasHeight) {
   8530
   8531            // Copy old data only if relevant and non-empty
   8532            var oldData = null;
   8533            if (!empty && canvas.width !== 0 && canvas.height !== 0) {
   8534
   8535                // Create canvas and context for holding old data
   8536                oldData = document.createElement("canvas");
   8537                oldData.width = Math.min(layer.width, newWidth);
   8538                oldData.height = Math.min(layer.height, newHeight);
   8539
   8540                var oldDataContext = oldData.getContext("2d");
   8541
   8542                // Copy image data from current
   8543                oldDataContext.drawImage(canvas,
   8544                        0, 0, oldData.width, oldData.height,
   8545                        0, 0, oldData.width, oldData.height);
   8546
   8547            }
   8548
   8549            // Preserve composite operation
   8550            var oldCompositeOperation = context.globalCompositeOperation;
   8551
   8552            // Resize canvas
   8553            canvas.width = canvasWidth;
   8554            canvas.height = canvasHeight;
   8555
   8556            // Redraw old data, if any
   8557            if (oldData)
   8558                context.drawImage(oldData,
   8559                    0, 0, oldData.width, oldData.height,
   8560                    0, 0, oldData.width, oldData.height);
   8561
   8562            // Restore composite operation
   8563            context.globalCompositeOperation = oldCompositeOperation;
   8564
   8565            // Acknowledge reset of stack (happens on resize of canvas)
   8566            stackSize = 0;
   8567            context.save();
   8568
   8569        }
   8570
   8571        // If the canvas size is not changing, manually force state reset
   8572        else
   8573            layer.reset();
   8574
   8575        // Assign new layer dimensions
   8576        layer.width = newWidth;
   8577        layer.height = newHeight;
   8578
   8579    };
   8580
   8581    /**
   8582     * Given the X and Y coordinates of the upper-left corner of a rectangle
   8583     * and the rectangle's width and height, resize the backing canvas element
   8584     * as necessary to ensure that the rectangle fits within the canvas
   8585     * element's coordinate space. This function will only make the canvas
   8586     * larger. If the rectangle already fits within the canvas element's
   8587     * coordinate space, the canvas is left unchanged.
   8588     * 
   8589     * @private
   8590     * @param {!number} x
   8591     *     The X coordinate of the upper-left corner of the rectangle to fit.
   8592     *
   8593     * @param {!number} y
   8594     *     The Y coordinate of the upper-left corner of the rectangle to fit.
   8595     *
   8596     * @param {!number} w
   8597     *     The width of the rectangle to fit.
   8598     *
   8599     * @param {!number} h
   8600     *     The height of the rectangle to fit.
   8601     */
   8602    function fitRect(x, y, w, h) {
   8603        
   8604        // Calculate bounds
   8605        var opBoundX = w + x;
   8606        var opBoundY = h + y;
   8607        
   8608        // Determine max width
   8609        var resizeWidth;
   8610        if (opBoundX > layer.width)
   8611            resizeWidth = opBoundX;
   8612        else
   8613            resizeWidth = layer.width;
   8614
   8615        // Determine max height
   8616        var resizeHeight;
   8617        if (opBoundY > layer.height)
   8618            resizeHeight = opBoundY;
   8619        else
   8620            resizeHeight = layer.height;
   8621
   8622        // Resize if necessary
   8623        layer.resize(resizeWidth, resizeHeight);
   8624
   8625    }
   8626
   8627    /**
   8628     * Set to true if this Layer should resize itself to accommodate the
   8629     * dimensions of any drawing operation, and false (the default) otherwise.
   8630     * 
   8631     * Note that setting this property takes effect immediately, and thus may
   8632     * take effect on operations that were started in the past but have not
   8633     * yet completed. If you wish the setting of this flag to only modify
   8634     * future operations, you will need to make the setting of this flag an
   8635     * operation with sync().
   8636     * 
   8637     * @example
   8638     * // Set autosize to true for all future operations
   8639     * layer.sync(function() {
   8640     *     layer.autosize = true;
   8641     * });
   8642     * 
   8643     * @type {!boolean}
   8644     * @default false
   8645     */
   8646    this.autosize = false;
   8647
   8648    /**
   8649     * The current width of this layer.
   8650     *
   8651     * @type {!number}
   8652     */
   8653    this.width = width;
   8654
   8655    /**
   8656     * The current height of this layer.
   8657     *
   8658     * @type {!number}
   8659     */
   8660    this.height = height;
   8661
   8662    /**
   8663     * Returns the canvas element backing this Layer. Note that the dimensions
   8664     * of the canvas may not exactly match those of the Layer, as resizing a
   8665     * canvas while maintaining its state is an expensive operation.
   8666     *
   8667     * @returns {!HTMLCanvasElement}
   8668     *     The canvas element backing this Layer.
   8669     */
   8670    this.getCanvas = function getCanvas() {
   8671        return canvas;
   8672    };
   8673
   8674    /**
   8675     * Returns a new canvas element containing the same image as this Layer.
   8676     * Unlike getCanvas(), the canvas element returned is guaranteed to have
   8677     * the exact same dimensions as the Layer.
   8678     *
   8679     * @returns {!HTMLCanvasElement}
   8680     *     A new canvas element containing a copy of the image content this
   8681     *     Layer.
   8682     */
   8683    this.toCanvas = function toCanvas() {
   8684
   8685        // Create new canvas having same dimensions
   8686        var canvas = document.createElement('canvas');
   8687        canvas.width = layer.width;
   8688        canvas.height = layer.height;
   8689
   8690        // Copy image contents to new canvas
   8691        var context = canvas.getContext('2d');
   8692        context.drawImage(layer.getCanvas(), 0, 0);
   8693
   8694        return canvas;
   8695
   8696    };
   8697
   8698    /**
   8699     * Changes the size of this Layer to the given width and height. Resizing
   8700     * is only attempted if the new size provided is actually different from
   8701     * the current size.
   8702     * 
   8703     * @param {!number} newWidth
   8704     *     The new width to assign to this Layer.
   8705     *
   8706     * @param {!number} newHeight
   8707     *     The new height to assign to this Layer.
   8708     */
   8709    this.resize = function(newWidth, newHeight) {
   8710        if (newWidth !== layer.width || newHeight !== layer.height)
   8711            resize(newWidth, newHeight);
   8712    };
   8713
   8714    /**
   8715     * Draws the specified image at the given coordinates. The image specified
   8716     * must already be loaded.
   8717     * 
   8718     * @param {!number} x
   8719     *     The destination X coordinate.
   8720     *
   8721     * @param {!number} y
   8722     *     The destination Y coordinate.
   8723     *
   8724     * @param {!CanvasImageSource} image
   8725     *     The image to draw. Note that this is not a URL.
   8726     */
   8727    this.drawImage = function(x, y, image) {
   8728        if (layer.autosize) fitRect(x, y, image.width, image.height);
   8729        context.drawImage(image, x, y);
   8730        empty = false;
   8731    };
   8732
   8733    /**
   8734     * Transfer a rectangle of image data from one Layer to this Layer using the
   8735     * specified transfer function.
   8736     * 
   8737     * @param {!Guacamole.Layer} srcLayer
   8738     *     The Layer to copy image data from.
   8739     *
   8740     * @param {!number} srcx
   8741     *     The X coordinate of the upper-left corner of the rectangle within
   8742     *     the source Layer's coordinate space to copy data from.
   8743     *
   8744     * @param {!number} srcy
   8745     *     The Y coordinate of the upper-left corner of the rectangle within
   8746     *     the source Layer's coordinate space to copy data from.
   8747     *
   8748     * @param {!number} srcw
   8749     *     The width of the rectangle within the source Layer's coordinate
   8750     *     space to copy data from.
   8751     *
   8752     * @param {!number} srch
   8753     *     The height of the rectangle within the source Layer's coordinate
   8754     *     space to copy data from.
   8755     *
   8756     * @param {!number} x
   8757     *     The destination X coordinate.
   8758     *
   8759     * @param {!number} y
   8760     *     The destination Y coordinate.
   8761     *
   8762     * @param {!function} transferFunction
   8763     *     The transfer function to use to transfer data from source to
   8764     *     destination.
   8765     */
   8766    this.transfer = function(srcLayer, srcx, srcy, srcw, srch, x, y, transferFunction) {
   8767
   8768        var srcCanvas = srcLayer.getCanvas();
   8769
   8770        // If entire rectangle outside source canvas, stop
   8771        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;
   8772
   8773        // Otherwise, clip rectangle to area
   8774        if (srcx + srcw > srcCanvas.width)
   8775            srcw = srcCanvas.width - srcx;
   8776
   8777        if (srcy + srch > srcCanvas.height)
   8778            srch = srcCanvas.height - srcy;
   8779
   8780        // Stop if nothing to draw.
   8781        if (srcw === 0 || srch === 0) return;
   8782
   8783        if (layer.autosize) fitRect(x, y, srcw, srch);
   8784
   8785        // Get image data from src and dst
   8786        var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch);
   8787        var dst = context.getImageData(x , y, srcw, srch);
   8788
   8789        // Apply transfer for each pixel
   8790        for (var i=0; i<srcw*srch*4; i+=4) {
   8791
   8792            // Get source pixel environment
   8793            var src_pixel = new Guacamole.Layer.Pixel(
   8794                src.data[i],
   8795                src.data[i+1],
   8796                src.data[i+2],
   8797                src.data[i+3]
   8798            );
   8799                
   8800            // Get destination pixel environment
   8801            var dst_pixel = new Guacamole.Layer.Pixel(
   8802                dst.data[i],
   8803                dst.data[i+1],
   8804                dst.data[i+2],
   8805                dst.data[i+3]
   8806            );
   8807
   8808            // Apply transfer function
   8809            transferFunction(src_pixel, dst_pixel);
   8810
   8811            // Save pixel data
   8812            dst.data[i  ] = dst_pixel.red;
   8813            dst.data[i+1] = dst_pixel.green;
   8814            dst.data[i+2] = dst_pixel.blue;
   8815            dst.data[i+3] = dst_pixel.alpha;
   8816
   8817        }
   8818
   8819        // Draw image data
   8820        context.putImageData(dst, x, y);
   8821        empty = false;
   8822
   8823    };
   8824
   8825    /**
   8826     * Put a rectangle of image data from one Layer to this Layer directly
   8827     * without performing any alpha blending. Simply copy the data.
   8828     * 
   8829     * @param {!Guacamole.Layer} srcLayer
   8830     *     The Layer to copy image data from.
   8831     *
   8832     * @param {!number} srcx
   8833     *     The X coordinate of the upper-left corner of the rectangle within
   8834     *     the source Layer's coordinate space to copy data from.
   8835     *
   8836     * @param {!number} srcy
   8837     *     The Y coordinate of the upper-left corner of the rectangle within
   8838     *     the source Layer's coordinate space to copy data from.
   8839     *
   8840     * @param {!number} srcw
   8841     *     The width of the rectangle within the source Layer's coordinate
   8842     *     space to copy data from.
   8843     *
   8844     * @param {!number} srch
   8845     *     The height of the rectangle within the source Layer's coordinate
   8846     *     space to copy data from.
   8847     *
   8848     * @param {!number} x
   8849     *     The destination X coordinate.
   8850     *
   8851     * @param {!number} y
   8852     *     The destination Y coordinate.
   8853     */
   8854    this.put = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
   8855
   8856        var srcCanvas = srcLayer.getCanvas();
   8857
   8858        // If entire rectangle outside source canvas, stop
   8859        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;
   8860
   8861        // Otherwise, clip rectangle to area
   8862        if (srcx + srcw > srcCanvas.width)
   8863            srcw = srcCanvas.width - srcx;
   8864
   8865        if (srcy + srch > srcCanvas.height)
   8866            srch = srcCanvas.height - srcy;
   8867
   8868        // Stop if nothing to draw.
   8869        if (srcw === 0 || srch === 0) return;
   8870
   8871        if (layer.autosize) fitRect(x, y, srcw, srch);
   8872
   8873        // Get image data from src and dst
   8874        var src = srcLayer.getCanvas().getContext("2d").getImageData(srcx, srcy, srcw, srch);
   8875        context.putImageData(src, x, y);
   8876        empty = false;
   8877
   8878    };
   8879
   8880    /**
   8881     * Copy a rectangle of image data from one Layer to this Layer. This
   8882     * operation will copy exactly the image data that will be drawn once all
   8883     * operations of the source Layer that were pending at the time this
   8884     * function was called are complete. This operation will not alter the
   8885     * size of the source Layer even if its autosize property is set to true.
   8886     * 
   8887     * @param {!Guacamole.Layer} srcLayer
   8888     *     The Layer to copy image data from.
   8889     *
   8890     * @param {!number} srcx
   8891     *     The X coordinate of the upper-left corner of the rectangle within
   8892     *     the source Layer's coordinate space to copy data from.
   8893     *
   8894     * @param {!number} srcy
   8895     *     The Y coordinate of the upper-left corner of the rectangle within
   8896     *     the source Layer's coordinate space to copy data from.
   8897     *
   8898     * @param {!number} srcw
   8899     *     The width of the rectangle within the source Layer's coordinate
   8900     *     space to copy data from.
   8901     *
   8902     * @param {!number} srch
   8903     *     The height of the rectangle within the source Layer's coordinate
   8904     *     space to copy data from.
   8905     *
   8906     * @param {!number} x
   8907     *     The destination X coordinate.
   8908     *
   8909     * @param {!number} y
   8910     *     The destination Y coordinate.
   8911     */
   8912    this.copy = function(srcLayer, srcx, srcy, srcw, srch, x, y) {
   8913
   8914        var srcCanvas = srcLayer.getCanvas();
   8915
   8916        // If entire rectangle outside source canvas, stop
   8917        if (srcx >= srcCanvas.width || srcy >= srcCanvas.height) return;
   8918
   8919        // Otherwise, clip rectangle to area
   8920        if (srcx + srcw > srcCanvas.width)
   8921            srcw = srcCanvas.width - srcx;
   8922
   8923        if (srcy + srch > srcCanvas.height)
   8924            srch = srcCanvas.height - srcy;
   8925
   8926        // Stop if nothing to draw.
   8927        if (srcw === 0 || srch === 0) return;
   8928
   8929        if (layer.autosize) fitRect(x, y, srcw, srch);
   8930        context.drawImage(srcCanvas, srcx, srcy, srcw, srch, x, y, srcw, srch);
   8931        empty = false;
   8932
   8933    };
   8934
   8935    /**
   8936     * Starts a new path at the specified point.
   8937     * 
   8938     * @param {!number} x
   8939     *     The X coordinate of the point to draw.
   8940     *
   8941     * @param {!number} y
   8942     *     The Y coordinate of the point to draw.
   8943     */
   8944    this.moveTo = function(x, y) {
   8945        
   8946        // Start a new path if current path is closed
   8947        if (pathClosed) {
   8948            context.beginPath();
   8949            pathClosed = false;
   8950        }
   8951        
   8952        if (layer.autosize) fitRect(x, y, 0, 0);
   8953        context.moveTo(x, y);
   8954
   8955    };
   8956
   8957    /**
   8958     * Add the specified line to the current path.
   8959     * 
   8960     * @param {!number} x
   8961     *     The X coordinate of the endpoint of the line to draw.
   8962     *
   8963     * @param {!number} y
   8964     *     The Y coordinate of the endpoint of the line to draw.
   8965     */
   8966    this.lineTo = function(x, y) {
   8967        
   8968        // Start a new path if current path is closed
   8969        if (pathClosed) {
   8970            context.beginPath();
   8971            pathClosed = false;
   8972        }
   8973        
   8974        if (layer.autosize) fitRect(x, y, 0, 0);
   8975        context.lineTo(x, y);
   8976        
   8977    };
   8978
   8979    /**
   8980     * Add the specified arc to the current path.
   8981     * 
   8982     * @param {!number} x
   8983     *     The X coordinate of the center of the circle which will contain the
   8984     *     arc.
   8985     *
   8986     * @param {!number} y
   8987     *     The Y coordinate of the center of the circle which will contain the
   8988     *     arc.
   8989     *
   8990     * @param {!number} radius
   8991     *     The radius of the circle.
   8992     *
   8993     * @param {!number} startAngle
   8994     *     The starting angle of the arc, in radians.
   8995     *
   8996     * @param {!number} endAngle
   8997     *     The ending angle of the arc, in radians.
   8998     *
   8999     * @param {!boolean} negative
   9000     *     Whether the arc should be drawn in order of decreasing angle.
   9001     */
   9002    this.arc = function(x, y, radius, startAngle, endAngle, negative) {
   9003        
   9004        // Start a new path if current path is closed
   9005        if (pathClosed) {
   9006            context.beginPath();
   9007            pathClosed = false;
   9008        }
   9009        
   9010        if (layer.autosize) fitRect(x, y, 0, 0);
   9011        context.arc(x, y, radius, startAngle, endAngle, negative);
   9012        
   9013    };
   9014
   9015    /**
   9016     * Starts a new path at the specified point.
   9017     * 
   9018     * @param {!number} cp1x
   9019     *     The X coordinate of the first control point.
   9020     *
   9021     * @param {!number} cp1y
   9022     *     The Y coordinate of the first control point.
   9023     *
   9024     * @param {!number} cp2x
   9025     *     The X coordinate of the second control point.
   9026     *
   9027     * @param {!number} cp2y
   9028     *     The Y coordinate of the second control point.
   9029     *
   9030     * @param {!number} x
   9031     *     The X coordinate of the endpoint of the curve.
   9032     *
   9033     * @param {!number} y
   9034     *     The Y coordinate of the endpoint of the curve.
   9035     */
   9036    this.curveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {
   9037        
   9038        // Start a new path if current path is closed
   9039        if (pathClosed) {
   9040            context.beginPath();
   9041            pathClosed = false;
   9042        }
   9043        
   9044        if (layer.autosize) fitRect(x, y, 0, 0);
   9045        context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
   9046        
   9047    };
   9048
   9049    /**
   9050     * Closes the current path by connecting the end point with the start
   9051     * point (if any) with a straight line.
   9052     */
   9053    this.close = function() {
   9054        context.closePath();
   9055        pathClosed = true;
   9056    };
   9057
   9058    /**
   9059     * Add the specified rectangle to the current path.
   9060     * 
   9061     * @param {!number} x
   9062     *     The X coordinate of the upper-left corner of the rectangle to draw.
   9063     *
   9064     * @param {!number} y
   9065     *     The Y coordinate of the upper-left corner of the rectangle to draw.
   9066     *
   9067     * @param {!number} w
   9068     *     The width of the rectangle to draw.
   9069     *
   9070     * @param {!number} h
   9071     *     The height of the rectangle to draw.
   9072     */
   9073    this.rect = function(x, y, w, h) {
   9074            
   9075        // Start a new path if current path is closed
   9076        if (pathClosed) {
   9077            context.beginPath();
   9078            pathClosed = false;
   9079        }
   9080        
   9081        if (layer.autosize) fitRect(x, y, w, h);
   9082        context.rect(x, y, w, h);
   9083        
   9084    };
   9085
   9086    /**
   9087     * Clip all future drawing operations by the current path. The current path
   9088     * is implicitly closed. The current path can continue to be reused
   9089     * for other operations (such as fillColor()) but a new path will be started
   9090     * once a path drawing operation (path() or rect()) is used.
   9091     */
   9092    this.clip = function() {
   9093
   9094        // Set new clipping region
   9095        context.clip();
   9096
   9097        // Path now implicitly closed
   9098        pathClosed = true;
   9099
   9100    };
   9101
   9102    /**
   9103     * Stroke the current path with the specified color. The current path
   9104     * is implicitly closed. The current path can continue to be reused
   9105     * for other operations (such as clip()) but a new path will be started
   9106     * once a path drawing operation (path() or rect()) is used.
   9107     * 
   9108     * @param {!string} cap
   9109     *     The line cap style. Can be "round", "square", or "butt".
   9110     *
   9111     * @param {!string} join
   9112     *     The line join style. Can be "round", "bevel", or "miter".
   9113     *
   9114     * @param {!number} thickness
   9115     *     The line thickness in pixels.
   9116     *
   9117     * @param {!number} r
   9118     *     The red component of the color to fill.
   9119     *
   9120     * @param {!number} g
   9121     *     The green component of the color to fill.
   9122     *
   9123     * @param {!number} b
   9124     *     The blue component of the color to fill.
   9125     *
   9126     * @param {!number} a
   9127     *     The alpha component of the color to fill.
   9128     */
   9129    this.strokeColor = function(cap, join, thickness, r, g, b, a) {
   9130
   9131        // Stroke with color
   9132        context.lineCap = cap;
   9133        context.lineJoin = join;
   9134        context.lineWidth = thickness;
   9135        context.strokeStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
   9136        context.stroke();
   9137        empty = false;
   9138
   9139        // Path now implicitly closed
   9140        pathClosed = true;
   9141
   9142    };
   9143
   9144    /**
   9145     * Fills the current path with the specified color. The current path
   9146     * is implicitly closed. The current path can continue to be reused
   9147     * for other operations (such as clip()) but a new path will be started
   9148     * once a path drawing operation (path() or rect()) is used.
   9149     * 
   9150     * @param {!number} r
   9151     *     The red component of the color to fill.
   9152     *
   9153     * @param {!number} g
   9154     *     The green component of the color to fill.
   9155     *
   9156     * @param {!number} b
   9157     *     The blue component of the color to fill.
   9158     *
   9159     * @param {!number} a
   9160     *     The alpha component of the color to fill.
   9161     */
   9162    this.fillColor = function(r, g, b, a) {
   9163
   9164        // Fill with color
   9165        context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + a/255.0 + ")";
   9166        context.fill();
   9167        empty = false;
   9168
   9169        // Path now implicitly closed
   9170        pathClosed = true;
   9171
   9172    };
   9173
   9174    /**
   9175     * Stroke the current path with the image within the specified layer. The
   9176     * image data will be tiled infinitely within the stroke. The current path
   9177     * is implicitly closed. The current path can continue to be reused
   9178     * for other operations (such as clip()) but a new path will be started
   9179     * once a path drawing operation (path() or rect()) is used.
   9180     * 
   9181     * @param {!string} cap
   9182     *     The line cap style. Can be "round", "square", or "butt".
   9183     *
   9184     * @param {!string} join
   9185     *     The line join style. Can be "round", "bevel", or "miter".
   9186     *
   9187     * @param {!number} thickness
   9188     *     The line thickness in pixels.
   9189     *
   9190     * @param {!Guacamole.Layer} srcLayer
   9191     *     The layer to use as a repeating pattern within the stroke.
   9192     */
   9193    this.strokeLayer = function(cap, join, thickness, srcLayer) {
   9194
   9195        // Stroke with image data
   9196        context.lineCap = cap;
   9197        context.lineJoin = join;
   9198        context.lineWidth = thickness;
   9199        context.strokeStyle = context.createPattern(
   9200            srcLayer.getCanvas(),
   9201            "repeat"
   9202        );
   9203        context.stroke();
   9204        empty = false;
   9205
   9206        // Path now implicitly closed
   9207        pathClosed = true;
   9208
   9209    };
   9210
   9211    /**
   9212     * Fills the current path with the image within the specified layer. The
   9213     * image data will be tiled infinitely within the stroke. The current path
   9214     * is implicitly closed. The current path can continue to be reused
   9215     * for other operations (such as clip()) but a new path will be started
   9216     * once a path drawing operation (path() or rect()) is used.
   9217     * 
   9218     * @param {!Guacamole.Layer} srcLayer
   9219     *     The layer to use as a repeating pattern within the fill.
   9220     */
   9221    this.fillLayer = function(srcLayer) {
   9222
   9223        // Fill with image data 
   9224        context.fillStyle = context.createPattern(
   9225            srcLayer.getCanvas(),
   9226            "repeat"
   9227        );
   9228        context.fill();
   9229        empty = false;
   9230
   9231        // Path now implicitly closed
   9232        pathClosed = true;
   9233
   9234    };
   9235
   9236    /**
   9237     * Push current layer state onto stack.
   9238     */
   9239    this.push = function() {
   9240
   9241        // Save current state onto stack
   9242        context.save();
   9243        stackSize++;
   9244
   9245    };
   9246
   9247    /**
   9248     * Pop layer state off stack.
   9249     */
   9250    this.pop = function() {
   9251
   9252        // Restore current state from stack
   9253        if (stackSize > 0) {
   9254            context.restore();
   9255            stackSize--;
   9256        }
   9257
   9258    };
   9259
   9260    /**
   9261     * Reset the layer, clearing the stack, the current path, and any transform
   9262     * matrix.
   9263     */
   9264    this.reset = function() {
   9265
   9266        // Clear stack
   9267        while (stackSize > 0) {
   9268            context.restore();
   9269            stackSize--;
   9270        }
   9271
   9272        // Restore to initial state
   9273        context.restore();
   9274        context.save();
   9275
   9276        // Clear path
   9277        context.beginPath();
   9278        pathClosed = false;
   9279
   9280    };
   9281
   9282    /**
   9283     * Sets the given affine transform (defined with six values from the
   9284     * transform's matrix).
   9285     * 
   9286     * @param {!number} a
   9287     *     The first value in the affine transform's matrix.
   9288     *
   9289     * @param {!number} b
   9290     *     The second value in the affine transform's matrix.
   9291     *
   9292     * @param {!number} c
   9293     *     The third value in the affine transform's matrix.
   9294     *
   9295     * @param {!number} d
   9296     *     The fourth value in the affine transform's matrix.
   9297     *
   9298     * @param {!number} e
   9299     *     The fifth value in the affine transform's matrix.
   9300     *
   9301     * @param {!number} f
   9302     *     The sixth value in the affine transform's matrix.
   9303     */
   9304    this.setTransform = function(a, b, c, d, e, f) {
   9305        context.setTransform(
   9306            a, b, c,
   9307            d, e, f
   9308          /*0, 0, 1*/
   9309        );
   9310    };
   9311
   9312    /**
   9313     * Applies the given affine transform (defined with six values from the
   9314     * transform's matrix).
   9315     *
   9316     * @param {!number} a
   9317     *     The first value in the affine transform's matrix.
   9318     *
   9319     * @param {!number} b
   9320     *     The second value in the affine transform's matrix.
   9321     *
   9322     * @param {!number} c
   9323     *     The third value in the affine transform's matrix.
   9324     *
   9325     * @param {!number} d
   9326     *     The fourth value in the affine transform's matrix.
   9327     *
   9328     * @param {!number} e
   9329     *     The fifth value in the affine transform's matrix.
   9330     *
   9331     * @param {!number} f
   9332     *     The sixth value in the affine transform's matrix.
   9333     */
   9334    this.transform = function(a, b, c, d, e, f) {
   9335        context.transform(
   9336            a, b, c,
   9337            d, e, f
   9338          /*0, 0, 1*/
   9339        );
   9340    };
   9341
   9342    /**
   9343     * Sets the channel mask for future operations on this Layer.
   9344     * 
   9345     * The channel mask is a Guacamole-specific compositing operation identifier
   9346     * with a single bit representing each of four channels (in order): source
   9347     * image where destination transparent, source where destination opaque,
   9348     * destination where source transparent, and destination where source
   9349     * opaque.
   9350     * 
   9351     * @param {!number} mask
   9352     *     The channel mask for future operations on this Layer.
   9353     */
   9354    this.setChannelMask = function(mask) {
   9355        context.globalCompositeOperation = compositeOperation[mask];
   9356    };
   9357
   9358    /**
   9359     * Sets the miter limit for stroke operations using the miter join. This
   9360     * limit is the maximum ratio of the size of the miter join to the stroke
   9361     * width. If this ratio is exceeded, the miter will not be drawn for that
   9362     * joint of the path.
   9363     * 
   9364     * @param {!number} limit
   9365     *     The miter limit for stroke operations using the miter join.
   9366     */
   9367    this.setMiterLimit = function(limit) {
   9368        context.miterLimit = limit;
   9369    };
   9370
   9371    // Initialize canvas dimensions
   9372    resize(width, height);
   9373
   9374    // Explicitly render canvas below other elements in the layer (such as
   9375    // child layers). Chrome and others may fail to render layers properly
   9376    // without this.
   9377    canvas.style.zIndex = -1;
   9378
   9379};
   9380
   9381/**
   9382 * Channel mask for the composite operation "rout".
   9383 *
   9384 * @type {!number}
   9385 */
   9386Guacamole.Layer.ROUT  = 0x2;
   9387
   9388/**
   9389 * Channel mask for the composite operation "atop".
   9390 *
   9391 * @type {!number}
   9392 */
   9393Guacamole.Layer.ATOP  = 0x6;
   9394
   9395/**
   9396 * Channel mask for the composite operation "xor".
   9397 *
   9398 * @type {!number}
   9399 */
   9400Guacamole.Layer.XOR   = 0xA;
   9401
   9402/**
   9403 * Channel mask for the composite operation "rover".
   9404 *
   9405 * @type {!number}
   9406 */
   9407Guacamole.Layer.ROVER = 0xB;
   9408
   9409/**
   9410 * Channel mask for the composite operation "over".
   9411 *
   9412 * @type {!number}
   9413 */
   9414Guacamole.Layer.OVER  = 0xE;
   9415
   9416/**
   9417 * Channel mask for the composite operation "plus".
   9418 *
   9419 * @type {!number}
   9420 */
   9421Guacamole.Layer.PLUS  = 0xF;
   9422
   9423/**
   9424 * Channel mask for the composite operation "rin".
   9425 * Beware that WebKit-based browsers may leave the contents of the destination
   9426 * layer where the source layer is transparent, despite the definition of this
   9427 * operation.
   9428 *
   9429 * @type {!number}
   9430 */
   9431Guacamole.Layer.RIN   = 0x1;
   9432
   9433/**
   9434 * Channel mask for the composite operation "in".
   9435 * Beware that WebKit-based browsers may leave the contents of the destination
   9436 * layer where the source layer is transparent, despite the definition of this
   9437 * operation.
   9438 *
   9439 * @type {!number}
   9440 */
   9441Guacamole.Layer.IN    = 0x4;
   9442
   9443/**
   9444 * Channel mask for the composite operation "out".
   9445 * Beware that WebKit-based browsers may leave the contents of the destination
   9446 * layer where the source layer is transparent, despite the definition of this
   9447 * operation.
   9448 *
   9449 * @type {!number}
   9450 */
   9451Guacamole.Layer.OUT   = 0x8;
   9452
   9453/**
   9454 * Channel mask for the composite operation "ratop".
   9455 * Beware that WebKit-based browsers may leave the contents of the destination
   9456 * layer where the source layer is transparent, despite the definition of this
   9457 * operation.
   9458 *
   9459 * @type {!number}
   9460 */
   9461Guacamole.Layer.RATOP = 0x9;
   9462
   9463/**
   9464 * Channel mask for the composite operation "src".
   9465 * Beware that WebKit-based browsers may leave the contents of the destination
   9466 * layer where the source layer is transparent, despite the definition of this
   9467 * operation.
   9468 *
   9469 * @type {!number}
   9470 */
   9471Guacamole.Layer.SRC   = 0xC;
   9472
   9473/**
   9474 * Represents a single pixel of image data. All components have a minimum value
   9475 * of 0 and a maximum value of 255.
   9476 * 
   9477 * @constructor
   9478 * 
   9479 * @param {!number} r
   9480 *     The red component of this pixel.
   9481 *
   9482 * @param {!number} g
   9483 *     The green component of this pixel.
   9484 *
   9485 * @param {!number} b
   9486 *     The blue component of this pixel.
   9487 *
   9488 * @param {!number} a
   9489 *     The alpha component of this pixel.
   9490 */
   9491Guacamole.Layer.Pixel = function(r, g, b, a) {
   9492
   9493    /**
   9494     * The red component of this pixel, where 0 is the minimum value,
   9495     * and 255 is the maximum.
   9496     *
   9497     * @type {!number}
   9498     */
   9499    this.red   = r;
   9500
   9501    /**
   9502     * The green component of this pixel, where 0 is the minimum value,
   9503     * and 255 is the maximum.
   9504     *
   9505     * @type {!number}
   9506     */
   9507    this.green = g;
   9508
   9509    /**
   9510     * The blue component of this pixel, where 0 is the minimum value,
   9511     * and 255 is the maximum.
   9512     *
   9513     * @type {!number}
   9514     */
   9515    this.blue  = b;
   9516
   9517    /**
   9518     * The alpha component of this pixel, where 0 is the minimum value,
   9519     * and 255 is the maximum.
   9520     *
   9521     * @type {!number}
   9522     */
   9523    this.alpha = a;
   9524
   9525};
   9526/*
   9527 * Licensed to the Apache Software Foundation (ASF) under one
   9528 * or more contributor license agreements.  See the NOTICE file
   9529 * distributed with this work for additional information
   9530 * regarding copyright ownership.  The ASF licenses this file
   9531 * to you under the Apache License, Version 2.0 (the
   9532 * "License"); you may not use this file except in compliance
   9533 * with the License.  You may obtain a copy of the License at
   9534 *
   9535 *   http://www.apache.org/licenses/LICENSE-2.0
   9536 *
   9537 * Unless required by applicable law or agreed to in writing,
   9538 * software distributed under the License is distributed on an
   9539 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   9540 * KIND, either express or implied.  See the License for the
   9541 * specific language governing permissions and limitations
   9542 * under the License.
   9543 */
   9544
   9545var Guacamole = Guacamole || {};
   9546
   9547/**
   9548 * Provides cross-browser mouse events for a given element. The events of
   9549 * the given element are automatically populated with handlers that translate
   9550 * mouse events into a non-browser-specific event provided by the
   9551 * Guacamole.Mouse instance.
   9552 *
   9553 * @example
   9554 * var mouse = new Guacamole.Mouse(client.getDisplay().getElement());
   9555 *
   9556 * // Forward all mouse interaction over Guacamole connection
   9557 * mouse.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
   9558 *     client.sendMouseState(e.state, true);
   9559 * });
   9560 *
   9561 * @example
   9562 * // Hide software cursor when mouse leaves display
   9563 * mouse.on('mouseout', function hideCursor() {
   9564 *     client.getDisplay().showCursor(false);
   9565 * });
   9566 *
   9567 * @constructor
   9568 * @augments Guacamole.Mouse.Event.Target
   9569 * @param {!Element} element
   9570 *     The Element to use to provide mouse events.
   9571 */
   9572Guacamole.Mouse = function Mouse(element) {
   9573
   9574    Guacamole.Mouse.Event.Target.call(this);
   9575
   9576    /**
   9577     * Reference to this Guacamole.Mouse.
   9578     *
   9579     * @private
   9580     * @type {!Guacamole.Mouse}
   9581     */
   9582    var guac_mouse = this;
   9583
   9584    /**
   9585     * The number of mousemove events to require before re-enabling mouse
   9586     * event handling after receiving a touch event.
   9587     *
   9588     * @type {!number}
   9589     */
   9590    this.touchMouseThreshold = 3;
   9591
   9592    /**
   9593     * The minimum amount of pixels scrolled required for a single scroll button
   9594     * click.
   9595     *
   9596     * @type {!number}
   9597     */
   9598    this.scrollThreshold = 53;
   9599
   9600    /**
   9601     * The number of pixels to scroll per line.
   9602     *
   9603     * @type {!number}
   9604     */
   9605    this.PIXELS_PER_LINE = 18;
   9606
   9607    /**
   9608     * The number of pixels to scroll per page.
   9609     *
   9610     * @type {!number}
   9611     */
   9612    this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16;
   9613
   9614    /**
   9615     * Array of {@link Guacamole.Mouse.State} button names corresponding to the
   9616     * mouse button indices used by DOM mouse events.
   9617     *
   9618     * @private
   9619     * @type {!string[]}
   9620     */
   9621    var MOUSE_BUTTONS = [
   9622        Guacamole.Mouse.State.Buttons.LEFT,
   9623        Guacamole.Mouse.State.Buttons.MIDDLE,
   9624        Guacamole.Mouse.State.Buttons.RIGHT
   9625    ];
   9626
   9627    /**
   9628     * Counter of mouse events to ignore. This decremented by mousemove, and
   9629     * while non-zero, mouse events will have no effect.
   9630     *
   9631     * @private
   9632     * @type {!number}
   9633     */
   9634    var ignore_mouse = 0;
   9635
   9636    /**
   9637     * Cumulative scroll delta amount. This value is accumulated through scroll
   9638     * events and results in scroll button clicks if it exceeds a certain
   9639     * threshold.
   9640     *
   9641     * @private
   9642     * @type {!number}
   9643     */
   9644    var scroll_delta = 0;
   9645
   9646    // Block context menu so right-click gets sent properly
   9647    element.addEventListener("contextmenu", function(e) {
   9648        Guacamole.Event.DOMEvent.cancelEvent(e);
   9649    }, false);
   9650
   9651    element.addEventListener("mousemove", function(e) {
   9652
   9653        // If ignoring events, decrement counter
   9654        if (ignore_mouse) {
   9655            Guacamole.Event.DOMEvent.cancelEvent(e);
   9656            ignore_mouse--;
   9657            return;
   9658        }
   9659
   9660        guac_mouse.move(Guacamole.Position.fromClientPosition(element, e.clientX, e.clientY), e);
   9661
   9662    }, false);
   9663
   9664    element.addEventListener("mousedown", function(e) {
   9665
   9666        // Do not handle if ignoring events
   9667        if (ignore_mouse) {
   9668            Guacamole.Event.DOMEvent.cancelEvent(e);
   9669            return;
   9670        }
   9671
   9672        var button = MOUSE_BUTTONS[e.button];
   9673        if (button)
   9674            guac_mouse.press(button, e);
   9675
   9676    }, false);
   9677
   9678    element.addEventListener("mouseup", function(e) {
   9679
   9680        // Do not handle if ignoring events
   9681        if (ignore_mouse) {
   9682            Guacamole.Event.DOMEvent.cancelEvent(e);
   9683            return;
   9684        }
   9685
   9686        var button = MOUSE_BUTTONS[e.button];
   9687        if (button)
   9688            guac_mouse.release(button, e);
   9689
   9690    }, false);
   9691
   9692    element.addEventListener("mouseout", function(e) {
   9693
   9694        // Get parent of the element the mouse pointer is leaving
   9695       	if (!e) e = window.event;
   9696
   9697        // Check that mouseout is due to actually LEAVING the element
   9698        var target = e.relatedTarget || e.toElement;
   9699        while (target) {
   9700            if (target === element)
   9701                return;
   9702            target = target.parentNode;
   9703        }
   9704
   9705        // Release all buttons and fire mouseout
   9706        guac_mouse.reset(e);
   9707        guac_mouse.out(e);
   9708
   9709    }, false);
   9710
   9711    // Override selection on mouse event element.
   9712    element.addEventListener("selectstart", function(e) {
   9713        Guacamole.Event.DOMEvent.cancelEvent(e);
   9714    }, false);
   9715
   9716    // Ignore all pending mouse events when touch events are the apparent source
   9717    function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
   9718
   9719    element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
   9720    element.addEventListener("touchstart", ignorePendingMouseEvents, false);
   9721    element.addEventListener("touchend",   ignorePendingMouseEvents, false);
   9722
   9723    // Scroll wheel support
   9724    function mousewheel_handler(e) {
   9725
   9726        // Determine approximate scroll amount (in pixels)
   9727        var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
   9728
   9729        // If successfully retrieved scroll amount, convert to pixels if not
   9730        // already in pixels
   9731        if (delta) {
   9732
   9733            // Convert to pixels if delta was lines
   9734            if (e.deltaMode === 1)
   9735                delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
   9736
   9737            // Convert to pixels if delta was pages
   9738            else if (e.deltaMode === 2)
   9739                delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
   9740
   9741        }
   9742
   9743        // Otherwise, assume legacy mousewheel event and line scrolling
   9744        else
   9745            delta = e.detail * guac_mouse.PIXELS_PER_LINE;
   9746        
   9747        // Update overall delta
   9748        scroll_delta += delta;
   9749
   9750        // Up
   9751        if (scroll_delta <= -guac_mouse.scrollThreshold) {
   9752
   9753            // Repeatedly click the up button until insufficient delta remains
   9754            do {
   9755                guac_mouse.click(Guacamole.Mouse.State.Buttons.UP);
   9756                scroll_delta += guac_mouse.scrollThreshold;
   9757            } while (scroll_delta <= -guac_mouse.scrollThreshold);
   9758
   9759            // Reset delta
   9760            scroll_delta = 0;
   9761
   9762        }
   9763
   9764        // Down
   9765        if (scroll_delta >= guac_mouse.scrollThreshold) {
   9766
   9767            // Repeatedly click the down button until insufficient delta remains
   9768            do {
   9769                guac_mouse.click(Guacamole.Mouse.State.Buttons.DOWN);
   9770                scroll_delta -= guac_mouse.scrollThreshold;
   9771            } while (scroll_delta >= guac_mouse.scrollThreshold);
   9772
   9773            // Reset delta
   9774            scroll_delta = 0;
   9775
   9776        }
   9777
   9778        // All scroll/wheel events must currently be cancelled regardless of
   9779        // whether the dispatched event is cancelled, as there is no Guacamole
   9780        // scroll event and thus no way to cancel scroll events that are
   9781        // smaller than required to produce an up/down click
   9782        Guacamole.Event.DOMEvent.cancelEvent(e);
   9783
   9784    }
   9785
   9786    element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
   9787    element.addEventListener('mousewheel',     mousewheel_handler, false);
   9788    element.addEventListener('wheel',          mousewheel_handler, false);
   9789
   9790    /**
   9791     * Whether the browser supports CSS3 cursor styling, including hotspot
   9792     * coordinates.
   9793     *
   9794     * @private
   9795     * @type {!boolean}
   9796     */
   9797    var CSS3_CURSOR_SUPPORTED = (function() {
   9798
   9799        var div = document.createElement("div");
   9800
   9801        // If no cursor property at all, then no support
   9802        if (!("cursor" in div.style))
   9803            return false;
   9804
   9805        try {
   9806            // Apply simple 1x1 PNG
   9807            div.style.cursor = "url(data:image/png;base64,"
   9808                             + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
   9809                             + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI"
   9810                             + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA"
   9811                             + "AABJRU5ErkJggg==) 0 0, auto";
   9812        }
   9813        catch (e) {
   9814            return false;
   9815        }
   9816
   9817        // Verify cursor property is set to URL with hotspot
   9818        return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || "");
   9819
   9820    })();
   9821
   9822    /**
   9823     * Changes the local mouse cursor to the given canvas, having the given
   9824     * hotspot coordinates. This affects styling of the element backing this
   9825     * Guacamole.Mouse only, and may fail depending on browser support for
   9826     * setting the mouse cursor.
   9827     * 
   9828     * If setting the local cursor is desired, it is up to the implementation
   9829     * to do something else, such as use the software cursor built into
   9830     * Guacamole.Display, if the local cursor cannot be set.
   9831     *
   9832     * @param {!HTMLCanvasElement} canvas
   9833     *     The cursor image.
   9834     *
   9835     * @param {!number} x
   9836     *     The X-coordinate of the cursor hotspot.
   9837     *
   9838     * @param {!number} y
   9839     *     The Y-coordinate of the cursor hotspot.
   9840     *
   9841     * @return {!boolean}
   9842     *     true if the cursor was successfully set, false if the cursor could
   9843     *     not be set for any reason.
   9844     */
   9845    this.setCursor = function(canvas, x, y) {
   9846
   9847        // Attempt to set via CSS3 cursor styling
   9848        if (CSS3_CURSOR_SUPPORTED) {
   9849            var dataURL = canvas.toDataURL('image/png');
   9850            element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto";
   9851            return true;
   9852        }
   9853
   9854        // Otherwise, setting cursor failed
   9855        return false;
   9856
   9857    };
   9858
   9859};
   9860
   9861/**
   9862 * The current state of a mouse, including position and buttons.
   9863 *
   9864 * @constructor
   9865 * @augments Guacamole.Position
   9866 * @param {Guacamole.Mouse.State|object} [template={}]
   9867 *     The object whose properties should be copied within the new
   9868 *     Guacamole.Mouse.State.
   9869 */
   9870Guacamole.Mouse.State = function State(template) {
   9871
   9872    /**
   9873     * Returns the template object that would be provided to the
   9874     * Guacamole.Mouse.State constructor to produce a new Guacamole.Mouse.State
   9875     * object with the properties specified. The order and type of arguments
   9876     * used by this function are identical to those accepted by the
   9877     * Guacamole.Mouse.State constructor of Apache Guacamole 1.3.0 and older.
   9878     *
   9879     * @private
   9880     * @param {!number} x
   9881     *     The X position of the mouse pointer in pixels.
   9882     *
   9883     * @param {!number} y
   9884     *     The Y position of the mouse pointer in pixels.
   9885     *
   9886     * @param {!boolean} left
   9887     *     Whether the left mouse button is pressed.
   9888     *
   9889     * @param {!boolean} middle
   9890     *     Whether the middle mouse button is pressed.
   9891     *
   9892     * @param {!boolean} right
   9893     *     Whether the right mouse button is pressed.
   9894     *
   9895     * @param {!boolean} up
   9896     *     Whether the up mouse button is pressed (the fourth button, usually
   9897     *     part of a scroll wheel).
   9898     *
   9899     * @param {!boolean} down
   9900     *     Whether the down mouse button is pressed (the fifth button, usually
   9901     *     part of a scroll wheel).
   9902     *
   9903     * @return {!object}
   9904     *     The equivalent template object that would be passed to the new
   9905     *     Guacamole.Mouse.State constructor.
   9906     */
   9907    var legacyConstructor = function legacyConstructor(x, y, left, middle, right, up, down) {
   9908        return {
   9909            x      : x,
   9910            y      : y,
   9911            left   : left,
   9912            middle : middle,
   9913            right  : right,
   9914            up     : up,
   9915            down   : down
   9916        };
   9917    };
   9918
   9919    // Accept old-style constructor, as well
   9920    if (arguments.length > 1)
   9921        template = legacyConstructor.apply(this, arguments);
   9922    else
   9923        template = template || {};
   9924
   9925    Guacamole.Position.call(this, template);
   9926
   9927    /**
   9928     * Whether the left mouse button is currently pressed.
   9929     *
   9930     * @type {!boolean}
   9931     * @default false
   9932     */
   9933    this.left = template.left || false;
   9934
   9935    /**
   9936     * Whether the middle mouse button is currently pressed.
   9937     *
   9938     * @type {!boolean}
   9939     * @default false
   9940     */
   9941    this.middle = template.middle || false;
   9942
   9943    /**
   9944     * Whether the right mouse button is currently pressed.
   9945     *
   9946     * @type {!boolean}
   9947     * @default false
   9948     */
   9949    this.right = template.right || false;
   9950
   9951    /**
   9952     * Whether the up mouse button is currently pressed. This is the fourth
   9953     * mouse button, associated with upward scrolling of the mouse scroll
   9954     * wheel.
   9955     *
   9956     * @type {!boolean}
   9957     * @default false
   9958     */
   9959    this.up = template.up || false;
   9960
   9961    /**
   9962     * Whether the down mouse button is currently pressed. This is the fifth 
   9963     * mouse button, associated with downward scrolling of the mouse scroll
   9964     * wheel.
   9965     *
   9966     * @type {!boolean}
   9967     * @default false
   9968     */
   9969    this.down = template.down || false;
   9970
   9971};
   9972
   9973/**
   9974 * All mouse buttons that may be represented by a
   9975 * {@link Guacamole.Mouse.State}. 
   9976 *
   9977 * @readonly
   9978 * @enum
   9979 */
   9980Guacamole.Mouse.State.Buttons = {
   9981
   9982    /**
   9983     * The name of the {@link Guacamole.Mouse.State} property representing the
   9984     * left mouse button.
   9985     *
   9986     * @constant
   9987     * @type {!string}
   9988     */
   9989    LEFT : 'left',
   9990
   9991    /**
   9992     * The name of the {@link Guacamole.Mouse.State} property representing the
   9993     * middle mouse button.
   9994     *
   9995     * @constant
   9996     * @type {!string}
   9997     */
   9998    MIDDLE : 'middle',
   9999
  10000    /**
  10001     * The name of the {@link Guacamole.Mouse.State} property representing the
  10002     * right mouse button.
  10003     *
  10004     * @constant
  10005     * @type {!string}
  10006     */
  10007    RIGHT : 'right',
  10008
  10009    /**
  10010     * The name of the {@link Guacamole.Mouse.State} property representing the
  10011     * up mouse button (the fourth mouse button, clicked when the mouse scroll
  10012     * wheel is scrolled up).
  10013     *
  10014     * @constant
  10015     * @type {!string}
  10016     */
  10017    UP : 'up',
  10018
  10019    /**
  10020     * The name of the {@link Guacamole.Mouse.State} property representing the
  10021     * down mouse button (the fifth mouse button, clicked when the mouse scroll
  10022     * wheel is scrolled up).
  10023     *
  10024     * @constant
  10025     * @type {!string}
  10026     */
  10027    DOWN : 'down'
  10028
  10029};
  10030
  10031/**
  10032 * Base event type for all mouse events. The mouse producing the event may be
  10033 * the user's local mouse (as with {@link Guacamole.Mouse}) or an emulated
  10034 * mouse (as with {@link Guacamole.Mouse.Touchpad}).
  10035 *
  10036 * @constructor
  10037 * @augments Guacamole.Event.DOMEvent
  10038 * @param {!string} type
  10039 *     The type name of the event ("mousedown", "mouseup", etc.)
  10040 *
  10041 * @param {!Guacamole.Mouse.State} state
  10042 *     The current mouse state.
  10043 *     
  10044 * @param {Event|Event[]} [events=[]]
  10045 *     The DOM events that are related to this event, if any.
  10046 */
  10047Guacamole.Mouse.Event = function MouseEvent(type, state, events) {
  10048
  10049    Guacamole.Event.DOMEvent.call(this, type, events);
  10050
  10051    /**
  10052     * The name of the event handler used by the Guacamole JavaScript API for
  10053     * this event prior to the migration to Guacamole.Event.Target.
  10054     *
  10055     * @private
  10056     * @constant
  10057     * @type {!string}
  10058     */
  10059    var legacyHandlerName = 'on' + this.type;
  10060
  10061    /**
  10062     * The current mouse state at the time this event was fired.
  10063     *
  10064     * @type {!Guacamole.Mouse.State}
  10065     */
  10066    this.state = state;
  10067
  10068    /**
  10069     * @inheritdoc
  10070     */
  10071    this.invokeLegacyHandler = function invokeLegacyHandler(target) {
  10072        if (target[legacyHandlerName]) {
  10073
  10074            this.preventDefault();
  10075            this.stopPropagation();
  10076
  10077            target[legacyHandlerName](this.state);
  10078
  10079        }
  10080    };
  10081
  10082};
  10083
  10084/**
  10085 * An object which can dispatch {@link Guacamole.Mouse.Event} objects
  10086 * representing mouse events. These mouse events may be produced from an actual
  10087 * mouse device (as with {@link Guacamole.Mouse}), from an emulated mouse
  10088 * device (as with {@link Guacamole.Mouse.Touchpad}, or may be programmatically
  10089 * generated (using functions like [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  10090 * [press()]{@link Guacamole.Mouse.Event.Target#press}, and
  10091 * [release()]{@link Guacamole.Mouse.Event.Target#release}).
  10092 * 
  10093 * @constructor
  10094 * @augments Guacamole.Event.Target
  10095 */
  10096Guacamole.Mouse.Event.Target = function MouseEventTarget() {
  10097
  10098    Guacamole.Event.Target.call(this);
  10099
  10100    /**
  10101     * The current mouse state. The properties of this state are updated when
  10102     * mouse events fire. This state object is also passed in as a parameter to
  10103     * the handler of any mouse events.
  10104     *
  10105     * @type {!Guacamole.Mouse.State}
  10106     */
  10107    this.currentState = new Guacamole.Mouse.State();
  10108
  10109    /**
  10110     * Fired whenever a mouse button is effectively pressed. Depending on the
  10111     * object dispatching the event, this can be due to a true mouse button
  10112     * press ({@link Guacamole.Mouse}), an emulated mouse button press from a
  10113     * touch gesture ({@link Guacamole.Mouse.Touchpad} and
  10114     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  10115     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  10116     * [press()]{@link Guacamole.Mouse.Event.Target#press}, or
  10117     * [click()]{@link Guacamole.Mouse.Event.Target#click}.
  10118     *
  10119     * @event Guacamole.Mouse.Event.Target#mousedown
  10120     * @param {!Guacamole.Mouse.Event} event
  10121     *     The mousedown event that was fired.
  10122     */
  10123
  10124    /**
  10125     * Fired whenever a mouse button is effectively released. Depending on the
  10126     * object dispatching the event, this can be due to a true mouse button
  10127     * release ({@link Guacamole.Mouse}), an emulated mouse button release from
  10128     * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
  10129     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  10130     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  10131     * [release()]{@link Guacamole.Mouse.Event.Target#release}, or
  10132     * [click()]{@link Guacamole.Mouse.Event.Target#click}.
  10133     *
  10134     * @event Guacamole.Mouse.Event.Target#mouseup
  10135     * @param {!Guacamole.Mouse.Event} event
  10136     *     The mouseup event that was fired.
  10137     */
  10138
  10139    /**
  10140     * Fired whenever the mouse pointer is effectively moved. Depending on the
  10141     * object dispatching the event, this can be due to true mouse movement
  10142     * ({@link Guacamole.Mouse}), emulated mouse movement from
  10143     * a touch gesture ({@link Guacamole.Mouse.Touchpad} and
  10144     * {@link Guacamole.Mouse.Touchscreen}), or may be programmatically
  10145     * generated through [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  10146     * or [move()]{@link Guacamole.Mouse.Event.Target#move}.
  10147     *
  10148     * @event Guacamole.Mouse.Event.Target#mousemove
  10149     * @param {!Guacamole.Mouse.Event} event
  10150     *     The mousemove event that was fired.
  10151     */
  10152
  10153    /**
  10154     * Fired whenever the mouse pointer leaves the boundaries of the element
  10155     * being monitored for interaction. This will only ever be automatically
  10156     * fired due to movement of an actual mouse device via
  10157     * {@link Guacamole.Mouse} unless programmatically generated through
  10158     * [dispatch()]{@link Guacamole.Mouse.Event.Target#dispatch},
  10159     * or [out()]{@link Guacamole.Mouse.Event.Target#out}.
  10160     *
  10161     * @event Guacamole.Mouse.Event.Target#mouseout
  10162     * @param {!Guacamole.Mouse.Event} event
  10163     *     The mouseout event that was fired.
  10164     */
  10165
  10166    /**
  10167     * Presses the given mouse button, if it isn't already pressed. Valid
  10168     * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
  10169     * correspond to the button-related properties of
  10170     * {@link Guacamole.Mouse.State}.
  10171     *
  10172     * @fires Guacamole.Mouse.Event.Target#mousedown
  10173     *
  10174     * @param {!string} button
  10175     *     The name of the mouse button to press, as defined by
  10176     *     {@link Guacamole.Mouse.State.Buttons}.
  10177     *
  10178     * @param {Event|Event[]} [events=[]]
  10179     *     The DOM events that are related to the mouse button press, if any.
  10180     */
  10181    this.press = function press(button, events) {
  10182        if (!this.currentState[button]) {
  10183            this.currentState[button] = true;
  10184            this.dispatch(new Guacamole.Mouse.Event('mousedown', this.currentState, events));
  10185        }
  10186    };
  10187
  10188    /**
  10189     * Releases the given mouse button, if it isn't already released. Valid
  10190     * button names are defined by {@link Guacamole.Mouse.State.Buttons} and
  10191     * correspond to the button-related properties of
  10192     * {@link Guacamole.Mouse.State}.
  10193     *
  10194     * @fires Guacamole.Mouse.Event.Target#mouseup
  10195     *
  10196     * @param {!string} button
  10197     *     The name of the mouse button to release, as defined by
  10198     *     {@link Guacamole.Mouse.State.Buttons}.
  10199     *
  10200     * @param {Event|Event[]} [events=[]]
  10201     *     The DOM events related to the mouse button release, if any.
  10202     */
  10203    this.release = function release(button, events) {
  10204        if (this.currentState[button]) {
  10205            this.currentState[button] = false;
  10206            this.dispatch(new Guacamole.Mouse.Event('mouseup', this.currentState, events));
  10207        }
  10208    };
  10209
  10210    /**
  10211     * Clicks (presses and releases) the given mouse button. Valid button
  10212     * names are defined by {@link Guacamole.Mouse.State.Buttons} and
  10213     * correspond to the button-related properties of
  10214     * {@link Guacamole.Mouse.State}.
  10215     *
  10216     * @fires Guacamole.Mouse.Event.Target#mousedown
  10217     * @fires Guacamole.Mouse.Event.Target#mouseup
  10218     *
  10219     * @param {!string} button
  10220     *     The name of the mouse button to click, as defined by
  10221     *     {@link Guacamole.Mouse.State.Buttons}.
  10222     *
  10223     * @param {Event|Event[]} [events=[]]
  10224     *     The DOM events related to the click, if any.
  10225     */
  10226    this.click = function click(button, events) {
  10227        this.press(button, events);
  10228        this.release(button, events);
  10229    };
  10230
  10231    /**
  10232     * Moves the mouse to the given coordinates.
  10233     *
  10234     * @fires Guacamole.Mouse.Event.Target#mousemove
  10235     *
  10236     * @param {!(Guacamole.Position|object)} position
  10237     *     The new coordinates of the mouse pointer. This object may be a
  10238     *     {@link Guacamole.Position} or any object with "x" and "y"
  10239     *     properties.
  10240     *
  10241     * @param {Event|Event[]} [events=[]]
  10242     *     The DOM events related to the mouse movement, if any.
  10243     */
  10244    this.move = function move(position, events) {
  10245
  10246        if (this.currentState.x !== position.x || this.currentState.y !== position.y) {
  10247            this.currentState.x = position.x;
  10248            this.currentState.y = position.y;
  10249            this.dispatch(new Guacamole.Mouse.Event('mousemove', this.currentState, events));
  10250        }
  10251
  10252    };
  10253
  10254    /**
  10255     * Notifies event listeners that the mouse pointer has left the boundaries
  10256     * of the area being monitored for mouse events.
  10257     *
  10258     * @fires Guacamole.Mouse.Event.Target#mouseout
  10259     *
  10260     * @param {Event|Event[]} [events=[]]
  10261     *     The DOM events related to the mouse leaving the boundaries of the
  10262     *     monitored object, if any.
  10263     */
  10264    this.out = function out(events) {
  10265        this.dispatch(new Guacamole.Mouse.Event('mouseout', this.currentState, events));
  10266    };
  10267
  10268    /**
  10269     * Releases all mouse buttons that are currently pressed. If all mouse
  10270     * buttons have already been released, this function has no effect.
  10271     *
  10272     * @fires Guacamole.Mouse.Event.Target#mouseup
  10273     *
  10274     * @param {Event|Event[]} [events=[]]
  10275     *     The DOM event related to all mouse buttons being released, if any.
  10276     */
  10277    this.reset = function reset(events) {
  10278        for (var button in Guacamole.Mouse.State.Buttons) {
  10279            this.release(Guacamole.Mouse.State.Buttons[button], events);
  10280        }
  10281    };
  10282
  10283};
  10284
  10285/**
  10286 * Provides cross-browser relative touch event translation for a given element.
  10287 * 
  10288 * Touch events are translated into mouse events as if the touches occurred
  10289 * on a touchpad (drag to push the mouse pointer, tap to click).
  10290 * 
  10291 * @example
  10292 * var touchpad = new Guacamole.Mouse.Touchpad(client.getDisplay().getElement());
  10293 *
  10294 * // Emulate a mouse using touchpad-style gestures, forwarding all mouse
  10295 * // interaction over Guacamole connection
  10296 * touchpad.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
  10297 *
  10298 *     // Re-show software mouse cursor if possibly hidden by a prior call to
  10299 *     // showCursor(), such as a "mouseout" event handler that hides the
  10300 *     // cursor
  10301 *     client.getDisplay().showCursor(true);
  10302 *
  10303 *     client.sendMouseState(e.state, true);
  10304 *
  10305 * });
  10306 *
  10307 * @constructor
  10308 * @augments Guacamole.Mouse.Event.Target
  10309 * @param {!Element} element
  10310 *     The Element to use to provide touch events.
  10311 */
  10312Guacamole.Mouse.Touchpad = function Touchpad(element) {
  10313
  10314    Guacamole.Mouse.Event.Target.call(this);
  10315
  10316    /**
  10317     * The "mouseout" event will never be fired by Guacamole.Mouse.Touchpad.
  10318     *
  10319     * @ignore
  10320     * @event Guacamole.Mouse.Touchpad#mouseout
  10321     */
  10322
  10323    /**
  10324     * Reference to this Guacamole.Mouse.Touchpad.
  10325     *
  10326     * @private
  10327     * @type {!Guacamole.Mouse.Touchpad}
  10328     */
  10329    var guac_touchpad = this;
  10330
  10331    /**
  10332     * The distance a two-finger touch must move per scrollwheel event, in
  10333     * pixels.
  10334     *
  10335     * @type {!number}
  10336     */
  10337    this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
  10338
  10339    /**
  10340     * The maximum number of milliseconds to wait for a touch to end for the
  10341     * gesture to be considered a click.
  10342     *
  10343     * @type {!number}
  10344     */
  10345    this.clickTimingThreshold = 250;
  10346
  10347    /**
  10348     * The maximum number of pixels to allow a touch to move for the gesture to
  10349     * be considered a click.
  10350     *
  10351     * @type {!number}
  10352     */
  10353    this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
  10354
  10355    /**
  10356     * The current mouse state. The properties of this state are updated when
  10357     * mouse events fire. This state object is also passed in as a parameter to
  10358     * the handler of any mouse events.
  10359     * 
  10360     * @type {!Guacamole.Mouse.State}
  10361     */
  10362    this.currentState = new Guacamole.Mouse.State();
  10363
  10364    var touch_count = 0;
  10365    var last_touch_x = 0;
  10366    var last_touch_y = 0;
  10367    var last_touch_time = 0;
  10368    var pixels_moved = 0;
  10369
  10370    var touch_buttons = {
  10371        1: "left",
  10372        2: "right",
  10373        3: "middle"
  10374    };
  10375
  10376    var gesture_in_progress = false;
  10377    var click_release_timeout = null;
  10378
  10379    element.addEventListener("touchend", function(e) {
  10380        
  10381        e.preventDefault();
  10382            
  10383        // If we're handling a gesture AND this is the last touch
  10384        if (gesture_in_progress && e.touches.length === 0) {
  10385            
  10386            var time = new Date().getTime();
  10387
  10388            // Get corresponding mouse button
  10389            var button = touch_buttons[touch_count];
  10390
  10391            // If mouse already down, release anad clear timeout
  10392            if (guac_touchpad.currentState[button]) {
  10393
  10394                // Fire button up event
  10395                guac_touchpad.release(button, e);
  10396
  10397                // Clear timeout, if set
  10398                if (click_release_timeout) {
  10399                    window.clearTimeout(click_release_timeout);
  10400                    click_release_timeout = null;
  10401                }
  10402
  10403            }
  10404
  10405            // If single tap detected (based on time and distance)
  10406            if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
  10407                    && pixels_moved < guac_touchpad.clickMoveThreshold) {
  10408
  10409                // Fire button down event
  10410                guac_touchpad.press(button, e);
  10411
  10412                // Delay mouse up - mouse up should be canceled if
  10413                // touchstart within timeout.
  10414                click_release_timeout = window.setTimeout(function() {
  10415                    
  10416                    // Fire button up event
  10417                    guac_touchpad.release(button, e);
  10418
  10419                    // Gesture now over
  10420                    gesture_in_progress = false;
  10421
  10422                }, guac_touchpad.clickTimingThreshold);
  10423
  10424            }
  10425
  10426            // If we're not waiting to see if this is a click, stop gesture
  10427            if (!click_release_timeout)
  10428                gesture_in_progress = false;
  10429
  10430        }
  10431
  10432    }, false);
  10433
  10434    element.addEventListener("touchstart", function(e) {
  10435
  10436        e.preventDefault();
  10437
  10438        // Track number of touches, but no more than three
  10439        touch_count = Math.min(e.touches.length, 3);
  10440
  10441        // Clear timeout, if set
  10442        if (click_release_timeout) {
  10443            window.clearTimeout(click_release_timeout);
  10444            click_release_timeout = null;
  10445        }
  10446
  10447        // Record initial touch location and time for touch movement
  10448        // and tap gestures
  10449        if (!gesture_in_progress) {
  10450
  10451            // Stop mouse events while touching
  10452            gesture_in_progress = true;
  10453
  10454            // Record touch location and time
  10455            var starting_touch = e.touches[0];
  10456            last_touch_x = starting_touch.clientX;
  10457            last_touch_y = starting_touch.clientY;
  10458            last_touch_time = new Date().getTime();
  10459            pixels_moved = 0;
  10460
  10461        }
  10462
  10463    }, false);
  10464
  10465    element.addEventListener("touchmove", function(e) {
  10466
  10467        e.preventDefault();
  10468
  10469        // Get change in touch location
  10470        var touch = e.touches[0];
  10471        var delta_x = touch.clientX - last_touch_x;
  10472        var delta_y = touch.clientY - last_touch_y;
  10473
  10474        // Track pixels moved
  10475        pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
  10476
  10477        // If only one touch involved, this is mouse move
  10478        if (touch_count === 1) {
  10479
  10480            // Calculate average velocity in Manhatten pixels per millisecond
  10481            var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
  10482
  10483            // Scale mouse movement relative to velocity
  10484            var scale = 1 + velocity;
  10485
  10486            // Update mouse location
  10487            var position = new Guacamole.Position(guac_touchpad.currentState);
  10488            position.x += delta_x*scale;
  10489            position.y += delta_y*scale;
  10490
  10491            // Prevent mouse from leaving screen
  10492            position.x = Math.min(Math.max(0, position.x), element.offsetWidth - 1);
  10493            position.y = Math.min(Math.max(0, position.y), element.offsetHeight - 1);
  10494
  10495            // Fire movement event, if defined
  10496            guac_touchpad.move(position, e);
  10497
  10498            // Update touch location
  10499            last_touch_x = touch.clientX;
  10500            last_touch_y = touch.clientY;
  10501
  10502        }
  10503
  10504        // Interpret two-finger swipe as scrollwheel
  10505        else if (touch_count === 2) {
  10506
  10507            // If change in location passes threshold for scroll
  10508            if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
  10509
  10510                // Decide button based on Y movement direction
  10511                var button;
  10512                if (delta_y > 0) button = "down";
  10513                else             button = "up";
  10514
  10515                guac_touchpad.click(button, e);
  10516
  10517                // Only update touch location after a scroll has been
  10518                // detected
  10519                last_touch_x = touch.clientX;
  10520                last_touch_y = touch.clientY;
  10521
  10522            }
  10523
  10524        }
  10525
  10526    }, false);
  10527
  10528};
  10529
  10530/**
  10531 * Provides cross-browser absolute touch event translation for a given element.
  10532 *
  10533 * Touch events are translated into mouse events as if the touches occurred
  10534 * on a touchscreen (tapping anywhere on the screen clicks at that point,
  10535 * long-press to right-click).
  10536 *
  10537 * @example
  10538 * var touchscreen = new Guacamole.Mouse.Touchscreen(client.getDisplay().getElement());
  10539 *
  10540 * // Emulate a mouse using touchscreen-style gestures, forwarding all mouse
  10541 * // interaction over Guacamole connection
  10542 * touchscreen.onEach(['mousedown', 'mousemove', 'mouseup'], function sendMouseEvent(e) {
  10543 *
  10544 *     // Re-show software mouse cursor if possibly hidden by a prior call to
  10545 *     // showCursor(), such as a "mouseout" event handler that hides the
  10546 *     // cursor
  10547 *     client.getDisplay().showCursor(true);
  10548 *
  10549 *     client.sendMouseState(e.state, true);
  10550 *
  10551 * });
  10552 *
  10553 * @constructor
  10554 * @augments Guacamole.Mouse.Event.Target
  10555 * @param {!Element} element
  10556 *     The Element to use to provide touch events.
  10557 */
  10558Guacamole.Mouse.Touchscreen = function Touchscreen(element) {
  10559
  10560    Guacamole.Mouse.Event.Target.call(this);
  10561
  10562    /**
  10563     * The "mouseout" event will never be fired by Guacamole.Mouse.Touchscreen.
  10564     *
  10565     * @ignore
  10566     * @event Guacamole.Mouse.Touchscreen#mouseout
  10567     */
  10568
  10569    /**
  10570     * Reference to this Guacamole.Mouse.Touchscreen.
  10571     *
  10572     * @private
  10573     * @type {!Guacamole.Mouse.Touchscreen}
  10574     */
  10575    var guac_touchscreen = this;
  10576
  10577    /**
  10578     * Whether a gesture is known to be in progress. If false, touch events
  10579     * will be ignored.
  10580     *
  10581     * @private
  10582     * @type {!boolean}
  10583     */
  10584    var gesture_in_progress = false;
  10585
  10586    /**
  10587     * The start X location of a gesture.
  10588     *
  10589     * @private
  10590     * @type {number}
  10591     */
  10592    var gesture_start_x = null;
  10593
  10594    /**
  10595     * The start Y location of a gesture.
  10596     *
  10597     * @private
  10598     * @type {number}
  10599     */
  10600    var gesture_start_y = null;
  10601
  10602    /**
  10603     * The timeout associated with the delayed, cancellable click release.
  10604     *
  10605     * @private
  10606     * @type {number}
  10607     */
  10608    var click_release_timeout = null;
  10609
  10610    /**
  10611     * The timeout associated with long-press for right click.
  10612     *
  10613     * @private
  10614     * @type {number}
  10615     */
  10616    var long_press_timeout = null;
  10617
  10618    /**
  10619     * The distance a two-finger touch must move per scrollwheel event, in
  10620     * pixels.
  10621     *
  10622     * @type {!number}
  10623     */
  10624    this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
  10625
  10626    /**
  10627     * The maximum number of milliseconds to wait for a touch to end for the
  10628     * gesture to be considered a click.
  10629     *
  10630     * @type {!number}
  10631     */
  10632    this.clickTimingThreshold = 250;
  10633
  10634    /**
  10635     * The maximum number of pixels to allow a touch to move for the gesture to
  10636     * be considered a click.
  10637     *
  10638     * @type {!number}
  10639     */
  10640    this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1);
  10641
  10642    /**
  10643     * The amount of time a press must be held for long press to be
  10644     * detected.
  10645     */
  10646    this.longPressThreshold = 500;
  10647
  10648    /**
  10649     * Returns whether the given touch event exceeds the movement threshold for
  10650     * clicking, based on where the touch gesture began.
  10651     *
  10652     * @private
  10653     * @param {!TouchEvent} e
  10654     *     The touch event to check.
  10655     *
  10656     * @return {!boolean}
  10657     *     true if the movement threshold is exceeded, false otherwise.
  10658     */
  10659    function finger_moved(e) {
  10660        var touch = e.touches[0] || e.changedTouches[0];
  10661        var delta_x = touch.clientX - gesture_start_x;
  10662        var delta_y = touch.clientY - gesture_start_y;
  10663        return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold;
  10664    }
  10665
  10666    /**
  10667     * Begins a new gesture at the location of the first touch in the given
  10668     * touch event.
  10669     * 
  10670     * @private
  10671     * @param {!TouchEvent} e
  10672     *     The touch event beginning this new gesture.
  10673     */
  10674    function begin_gesture(e) {
  10675        var touch = e.touches[0];
  10676        gesture_in_progress = true;
  10677        gesture_start_x = touch.clientX;
  10678        gesture_start_y = touch.clientY;
  10679    }
  10680
  10681    /**
  10682     * End the current gesture entirely. Wait for all touches to be done before
  10683     * resuming gesture detection.
  10684     * 
  10685     * @private
  10686     */
  10687    function end_gesture() {
  10688        window.clearTimeout(click_release_timeout);
  10689        window.clearTimeout(long_press_timeout);
  10690        gesture_in_progress = false;
  10691    }
  10692
  10693    element.addEventListener("touchend", function(e) {
  10694
  10695        // Do not handle if no gesture
  10696        if (!gesture_in_progress)
  10697            return;
  10698
  10699        // Ignore if more than one touch
  10700        if (e.touches.length !== 0 || e.changedTouches.length !== 1) {
  10701            end_gesture();
  10702            return;
  10703        }
  10704
  10705        // Long-press, if any, is over
  10706        window.clearTimeout(long_press_timeout);
  10707
  10708        // Always release mouse button if pressed
  10709        guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);
  10710
  10711        // If finger hasn't moved enough to cancel the click
  10712        if (!finger_moved(e)) {
  10713
  10714            e.preventDefault();
  10715
  10716            // If not yet pressed, press and start delay release
  10717            if (!guac_touchscreen.currentState.left) {
  10718
  10719                var touch = e.changedTouches[0];
  10720                guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
  10721                guac_touchscreen.press(Guacamole.Mouse.State.Buttons.LEFT, e);
  10722
  10723                // Release button after a delay, if not canceled
  10724                click_release_timeout = window.setTimeout(function() {
  10725                    guac_touchscreen.release(Guacamole.Mouse.State.Buttons.LEFT, e);
  10726                    end_gesture();
  10727                }, guac_touchscreen.clickTimingThreshold);
  10728
  10729            }
  10730
  10731        } // end if finger not moved
  10732
  10733    }, false);
  10734
  10735    element.addEventListener("touchstart", function(e) {
  10736
  10737        // Ignore if more than one touch
  10738        if (e.touches.length !== 1) {
  10739            end_gesture();
  10740            return;
  10741        }
  10742
  10743        e.preventDefault();
  10744
  10745        // New touch begins a new gesture
  10746        begin_gesture(e);
  10747
  10748        // Keep button pressed if tap after left click
  10749        window.clearTimeout(click_release_timeout);
  10750
  10751        // Click right button if this turns into a long-press
  10752        long_press_timeout = window.setTimeout(function() {
  10753            var touch = e.touches[0];
  10754            guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY));
  10755            guac_touchscreen.click(Guacamole.Mouse.State.Buttons.RIGHT, e);
  10756            end_gesture();
  10757        }, guac_touchscreen.longPressThreshold);
  10758
  10759    }, false);
  10760
  10761    element.addEventListener("touchmove", function(e) {
  10762
  10763        // Do not handle if no gesture
  10764        if (!gesture_in_progress)
  10765            return;
  10766
  10767        // Cancel long press if finger moved
  10768        if (finger_moved(e))
  10769            window.clearTimeout(long_press_timeout);
  10770
  10771        // Ignore if more than one touch
  10772        if (e.touches.length !== 1) {
  10773            end_gesture();
  10774            return;
  10775        }
  10776
  10777        // Update mouse position if dragging
  10778        if (guac_touchscreen.currentState.left) {
  10779
  10780            e.preventDefault();
  10781
  10782            // Update state
  10783            var touch = e.touches[0];
  10784            guac_touchscreen.move(Guacamole.Position.fromClientPosition(element, touch.clientX, touch.clientY), e);
  10785
  10786        }
  10787
  10788    }, false);
  10789
  10790};
  10791/*
  10792 * Licensed to the Apache Software Foundation (ASF) under one
  10793 * or more contributor license agreements.  See the NOTICE file
  10794 * distributed with this work for additional information
  10795 * regarding copyright ownership.  The ASF licenses this file
  10796 * to you under the Apache License, Version 2.0 (the
  10797 * "License"); you may not use this file except in compliance
  10798 * with the License.  You may obtain a copy of the License at
  10799 *
  10800 *   http://www.apache.org/licenses/LICENSE-2.0
  10801 *
  10802 * Unless required by applicable law or agreed to in writing,
  10803 * software distributed under the License is distributed on an
  10804 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  10805 * KIND, either express or implied.  See the License for the
  10806 * specific language governing permissions and limitations
  10807 * under the License.
  10808 */
  10809
  10810/**
  10811 * The namespace used by the Guacamole JavaScript API. Absolutely all classes
  10812 * defined by the Guacamole JavaScript API will be within this namespace.
  10813 *
  10814 * @namespace
  10815 */
  10816var Guacamole = Guacamole || {};
  10817/*
  10818 * Licensed to the Apache Software Foundation (ASF) under one
  10819 * or more contributor license agreements.  See the NOTICE file
  10820 * distributed with this work for additional information
  10821 * regarding copyright ownership.  The ASF licenses this file
  10822 * to you under the Apache License, Version 2.0 (the
  10823 * "License"); you may not use this file except in compliance
  10824 * with the License.  You may obtain a copy of the License at
  10825 *
  10826 *   http://www.apache.org/licenses/LICENSE-2.0
  10827 *
  10828 * Unless required by applicable law or agreed to in writing,
  10829 * software distributed under the License is distributed on an
  10830 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  10831 * KIND, either express or implied.  See the License for the
  10832 * specific language governing permissions and limitations
  10833 * under the License.
  10834 */
  10835
  10836var Guacamole = Guacamole || {};
  10837
  10838/**
  10839 * An object used by the Guacamole client to house arbitrarily-many named
  10840 * input and output streams.
  10841 * 
  10842 * @constructor
  10843 * @param {!Guacamole.Client} client
  10844 *     The client owning this object.
  10845 *
  10846 * @param {!number} index
  10847 *     The index of this object.
  10848 */
  10849Guacamole.Object = function guacamoleObject(client, index) {
  10850
  10851    /**
  10852     * Reference to this Guacamole.Object.
  10853     *
  10854     * @private
  10855     * @type {!Guacamole.Object}
  10856     */
  10857    var guacObject = this;
  10858
  10859    /**
  10860     * Map of stream name to corresponding queue of callbacks. The queue of
  10861     * callbacks is guaranteed to be in order of request.
  10862     *
  10863     * @private
  10864     * @type {!Object.<string, function[]>}
  10865     */
  10866    var bodyCallbacks = {};
  10867
  10868    /**
  10869     * Removes and returns the callback at the head of the callback queue for
  10870     * the stream having the given name. If no such callbacks exist, null is
  10871     * returned.
  10872     *
  10873     * @private
  10874     * @param {!string} name
  10875     *     The name of the stream to retrieve a callback for.
  10876     *
  10877     * @returns {function}
  10878     *     The next callback associated with the stream having the given name,
  10879     *     or null if no such callback exists.
  10880     */
  10881    var dequeueBodyCallback = function dequeueBodyCallback(name) {
  10882
  10883        // If no callbacks defined, simply return null
  10884        var callbacks = bodyCallbacks[name];
  10885        if (!callbacks)
  10886            return null;
  10887
  10888        // Otherwise, pull off first callback, deleting the queue if empty
  10889        var callback = callbacks.shift();
  10890        if (callbacks.length === 0)
  10891            delete bodyCallbacks[name];
  10892
  10893        // Return found callback
  10894        return callback;
  10895
  10896    };
  10897
  10898    /**
  10899     * Adds the given callback to the tail of the callback queue for the stream
  10900     * having the given name.
  10901     *
  10902     * @private
  10903     * @param {!string} name
  10904     *     The name of the stream to associate with the given callback.
  10905     *
  10906     * @param {!function} callback
  10907     *     The callback to add to the queue of the stream with the given name.
  10908     */
  10909    var enqueueBodyCallback = function enqueueBodyCallback(name, callback) {
  10910
  10911        // Get callback queue by name, creating first if necessary
  10912        var callbacks = bodyCallbacks[name];
  10913        if (!callbacks) {
  10914            callbacks = [];
  10915            bodyCallbacks[name] = callbacks;
  10916        }
  10917
  10918        // Add callback to end of queue
  10919        callbacks.push(callback);
  10920
  10921    };
  10922
  10923    /**
  10924     * The index of this object.
  10925     *
  10926     * @type {!number}
  10927     */
  10928    this.index = index;
  10929
  10930    /**
  10931     * Called when this object receives the body of a requested input stream.
  10932     * By default, all objects will invoke the callbacks provided to their
  10933     * requestInputStream() functions based on the name of the stream
  10934     * requested. This behavior can be overridden by specifying a different
  10935     * handler here.
  10936     *
  10937     * @event
  10938     * @param {!Guacamole.InputStream} inputStream
  10939     *     The input stream of the received body.
  10940     *
  10941     * @param {!string} mimetype
  10942     *     The mimetype of the data being received.
  10943     *
  10944     * @param {!string} name
  10945     *     The name of the stream whose body has been received.
  10946     */
  10947    this.onbody = function defaultBodyHandler(inputStream, mimetype, name) {
  10948
  10949        // Call queued callback for the received body, if any
  10950        var callback = dequeueBodyCallback(name);
  10951        if (callback)
  10952            callback(inputStream, mimetype);
  10953
  10954    };
  10955
  10956    /**
  10957     * Called when this object is being undefined. Once undefined, no further
  10958     * communication involving this object may occur.
  10959     * 
  10960     * @event
  10961     */
  10962    this.onundefine = null;
  10963
  10964    /**
  10965     * Requests read access to the input stream having the given name. If
  10966     * successful, a new input stream will be created.
  10967     *
  10968     * @param {!string} name
  10969     *     The name of the input stream to request.
  10970     *
  10971     * @param {function} [bodyCallback]
  10972     *     The callback to invoke when the body of the requested input stream
  10973     *     is received. This callback will be provided a Guacamole.InputStream
  10974     *     and its mimetype as its two only arguments. If the onbody handler of
  10975     *     this object is overridden, this callback will not be invoked.
  10976     */
  10977    this.requestInputStream = function requestInputStream(name, bodyCallback) {
  10978
  10979        // Queue body callback if provided
  10980        if (bodyCallback)
  10981            enqueueBodyCallback(name, bodyCallback);
  10982
  10983        // Send request for input stream
  10984        client.requestObjectInputStream(guacObject.index, name);
  10985
  10986    };
  10987
  10988    /**
  10989     * Creates a new output stream associated with this object and having the
  10990     * given mimetype and name. The legality of a mimetype and name is dictated
  10991     * by the object itself.
  10992     *
  10993     * @param {!string} mimetype
  10994     *     The mimetype of the data which will be sent to the output stream.
  10995     *
  10996     * @param {!string} name
  10997     *     The defined name of an output stream within this object.
  10998     *
  10999     * @returns {!Guacamole.OutputStream}
  11000     *     An output stream which will write blobs to the named output stream
  11001     *     of this object.
  11002     */
  11003    this.createOutputStream = function createOutputStream(mimetype, name) {
  11004        return client.createObjectOutputStream(guacObject.index, mimetype, name);
  11005    };
  11006
  11007};
  11008
  11009/**
  11010 * The reserved name denoting the root stream of any object. The contents of
  11011 * the root stream MUST be a JSON map of stream name to mimetype.
  11012 *
  11013 * @constant
  11014 * @type {!string}
  11015 */
  11016Guacamole.Object.ROOT_STREAM = '/';
  11017
  11018/**
  11019 * The mimetype of a stream containing JSON which maps available stream names
  11020 * to their corresponding mimetype. The root stream of a Guacamole.Object MUST
  11021 * have this mimetype.
  11022 *
  11023 * @constant
  11024 * @type {!string}
  11025 */
  11026Guacamole.Object.STREAM_INDEX_MIMETYPE = 'application/vnd.glyptodon.guacamole.stream-index+json';
  11027/*
  11028 * Licensed to the Apache Software Foundation (ASF) under one
  11029 * or more contributor license agreements.  See the NOTICE file
  11030 * distributed with this work for additional information
  11031 * regarding copyright ownership.  The ASF licenses this file
  11032 * to you under the Apache License, Version 2.0 (the
  11033 * "License"); you may not use this file except in compliance
  11034 * with the License.  You may obtain a copy of the License at
  11035 *
  11036 *   http://www.apache.org/licenses/LICENSE-2.0
  11037 *
  11038 * Unless required by applicable law or agreed to in writing,
  11039 * software distributed under the License is distributed on an
  11040 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  11041 * KIND, either express or implied.  See the License for the
  11042 * specific language governing permissions and limitations
  11043 * under the License.
  11044 */
  11045
  11046var Guacamole = Guacamole || {};
  11047
  11048/**
  11049 * Dynamic on-screen keyboard. Given the layout object for an on-screen
  11050 * keyboard, this object will construct a clickable on-screen keyboard with its
  11051 * own key events.
  11052 *
  11053 * @constructor
  11054 * @param {!Guacamole.OnScreenKeyboard.Layout} layout
  11055 *     The layout of the on-screen keyboard to display.
  11056 */
  11057Guacamole.OnScreenKeyboard = function(layout) {
  11058
  11059    /**
  11060     * Reference to this Guacamole.OnScreenKeyboard.
  11061     *
  11062     * @private
  11063     * @type {!Guacamole.OnScreenKeyboard}
  11064     */
  11065    var osk = this;
  11066
  11067    /**
  11068     * Map of currently-set modifiers to the keysym associated with their
  11069     * original press. When the modifier is cleared, this keysym must be
  11070     * released.
  11071     *
  11072     * @private
  11073     * @type {!Object.<String, Number>}
  11074     */
  11075    var modifierKeysyms = {};
  11076
  11077    /**
  11078     * Map of all key names to their current pressed states. If a key is not
  11079     * pressed, it may not be in this map at all, but all pressed keys will
  11080     * have a corresponding mapping to true.
  11081     *
  11082     * @private
  11083     * @type {!Object.<String, Boolean>}
  11084     */
  11085    var pressed = {};
  11086
  11087    /**
  11088     * All scalable elements which are part of the on-screen keyboard. Each
  11089     * scalable element is carefully controlled to ensure the interface layout
  11090     * and sizing remains constant, even on browsers that would otherwise
  11091     * experience rounding error due to unit conversions.
  11092     *
  11093     * @private
  11094     * @type {!ScaledElement[]}
  11095     */
  11096    var scaledElements = [];
  11097
  11098    /**
  11099     * Adds a CSS class to an element.
  11100     * 
  11101     * @private
  11102     * @function
  11103     * @param {!Element} element
  11104     *     The element to add a class to.
  11105     *
  11106     * @param {!string} classname
  11107     *     The name of the class to add.
  11108     */
  11109    var addClass = function addClass(element, classname) {
  11110
  11111        // If classList supported, use that
  11112        if (element.classList)
  11113            element.classList.add(classname);
  11114
  11115        // Otherwise, simply append the class
  11116        else
  11117            element.className += " " + classname;
  11118
  11119    };
  11120
  11121    /**
  11122     * Removes a CSS class from an element.
  11123     * 
  11124     * @private
  11125     * @function
  11126     * @param {!Element} element
  11127     *     The element to remove a class from.
  11128     *
  11129     * @param {!string} classname
  11130     *     The name of the class to remove.
  11131     */
  11132    var removeClass = function removeClass(element, classname) {
  11133
  11134        // If classList supported, use that
  11135        if (element.classList)
  11136            element.classList.remove(classname);
  11137
  11138        // Otherwise, manually filter out classes with given name
  11139        else {
  11140            element.className = element.className.replace(/([^ ]+)[ ]*/g,
  11141                function removeMatchingClasses(match, testClassname) {
  11142
  11143                    // If same class, remove
  11144                    if (testClassname === classname)
  11145                        return "";
  11146
  11147                    // Otherwise, allow
  11148                    return match;
  11149                    
  11150                }
  11151            );
  11152        }
  11153
  11154    };
  11155
  11156    /**
  11157     * Counter of mouse events to ignore. This decremented by mousemove, and
  11158     * while non-zero, mouse events will have no effect.
  11159     *
  11160     * @private
  11161     * @type {!number}
  11162     */
  11163    var ignoreMouse = 0;
  11164
  11165    /**
  11166     * Ignores all pending mouse events when touch events are the apparent
  11167     * source. Mouse events are ignored until at least touchMouseThreshold
  11168     * mouse events occur without corresponding touch events.
  11169     *
  11170     * @private
  11171     */
  11172    var ignorePendingMouseEvents = function ignorePendingMouseEvents() {
  11173        ignoreMouse = osk.touchMouseThreshold;
  11174    };
  11175
  11176    /**
  11177     * An element whose dimensions are maintained according to an arbitrary
  11178     * scale. The conversion factor for these arbitrary units to pixels is
  11179     * provided later via a call to scale().
  11180     *
  11181     * @private
  11182     * @constructor
  11183     * @param {!Element} element
  11184     *     The element whose scale should be maintained.
  11185     *
  11186     * @param {!number} width
  11187     *     The width of the element, in arbitrary units, relative to other
  11188     *     ScaledElements.
  11189     *
  11190     * @param {!number} height
  11191     *     The height of the element, in arbitrary units, relative to other
  11192     *     ScaledElements.
  11193     *     
  11194     * @param {boolean} [scaleFont=false]
  11195     *     Whether the line height and font size should be scaled as well.
  11196     */
  11197    var ScaledElement = function ScaledElement(element, width, height, scaleFont) {
  11198
  11199        /**
  11200         * The width of this ScaledElement, in arbitrary units, relative to
  11201         * other ScaledElements.
  11202         *
  11203         * @type {!number}
  11204         */
  11205         this.width = width;
  11206
  11207        /**
  11208         * The height of this ScaledElement, in arbitrary units, relative to
  11209         * other ScaledElements.
  11210         *
  11211         * @type {!number}
  11212         */
  11213         this.height = height;
  11214 
  11215        /**
  11216         * Resizes the associated element, updating its dimensions according to
  11217         * the given pixels per unit.
  11218         *
  11219         * @param {!number} pixels
  11220         *     The number of pixels to assign per arbitrary unit.
  11221         */
  11222        this.scale = function(pixels) {
  11223
  11224            // Scale element width/height
  11225            element.style.width  = (width  * pixels) + "px";
  11226            element.style.height = (height * pixels) + "px";
  11227
  11228            // Scale font, if requested
  11229            if (scaleFont) {
  11230                element.style.lineHeight = (height * pixels) + "px";
  11231                element.style.fontSize   = pixels + "px";
  11232            }
  11233
  11234        };
  11235
  11236    };
  11237
  11238    /**
  11239     * Returns whether all modifiers having the given names are currently
  11240     * active.
  11241     *
  11242     * @private
  11243     * @param {!string[]} names
  11244     *     The names of all modifiers to test.
  11245     *
  11246     * @returns {!boolean}
  11247     *     true if all specified modifiers are pressed, false otherwise.
  11248     */
  11249    var modifiersPressed = function modifiersPressed(names) {
  11250
  11251        // If any required modifiers are not pressed, return false
  11252        for (var i=0; i < names.length; i++) {
  11253
  11254            // Test whether current modifier is pressed
  11255            var name = names[i];
  11256            if (!(name in modifierKeysyms))
  11257                return false;
  11258
  11259        }
  11260
  11261        // Otherwise, all required modifiers are pressed
  11262        return true;
  11263
  11264    };
  11265
  11266    /**
  11267     * Returns the single matching Key object associated with the key of the
  11268     * given name, where that Key object's requirements (such as pressed
  11269     * modifiers) are all currently satisfied.
  11270     *
  11271     * @private
  11272     * @param {!string} keyName
  11273     *     The name of the key to retrieve.
  11274     *
  11275     * @returns {Guacamole.OnScreenKeyboard.Key}
  11276     *     The Key object associated with the given name, where that object's
  11277     *     requirements are all currently satisfied, or null if no such Key
  11278     *     can be found.
  11279     */
  11280    var getActiveKey = function getActiveKey(keyName) {
  11281
  11282        // Get key array for given name
  11283        var keys = osk.keys[keyName];
  11284        if (!keys)
  11285            return null;
  11286
  11287        // Find last matching key
  11288        for (var i = keys.length - 1; i >= 0; i--) {
  11289
  11290            // Get candidate key
  11291            var candidate = keys[i];
  11292
  11293            // If all required modifiers are pressed, use that key
  11294            if (modifiersPressed(candidate.requires))
  11295                return candidate;
  11296
  11297        }
  11298
  11299        // No valid key
  11300        return null;
  11301
  11302    };
  11303
  11304    /**
  11305     * Presses the key having the given name, updating the associated key
  11306     * element with the "guac-keyboard-pressed" CSS class. If the key is
  11307     * already pressed, this function has no effect.
  11308     *
  11309     * @private
  11310     * @param {!string} keyName
  11311     *     The name of the key to press.
  11312     *
  11313     * @param {!string} keyElement
  11314     *     The element associated with the given key.
  11315     */
  11316    var press = function press(keyName, keyElement) {
  11317
  11318        // Press key if not yet pressed
  11319        if (!pressed[keyName]) {
  11320
  11321            addClass(keyElement, "guac-keyboard-pressed");
  11322
  11323            // Get current key based on modifier state
  11324            var key = getActiveKey(keyName);
  11325
  11326            // Update modifier state
  11327            if (key.modifier) {
  11328
  11329                // Construct classname for modifier
  11330                var modifierClass = "guac-keyboard-modifier-" + getCSSName(key.modifier);
  11331
  11332                // Retrieve originally-pressed keysym, if modifier was already pressed
  11333                var originalKeysym = modifierKeysyms[key.modifier];
  11334
  11335                // Activate modifier if not pressed
  11336                if (originalKeysym === undefined) {
  11337                    
  11338                    addClass(keyboard, modifierClass);
  11339                    modifierKeysyms[key.modifier] = key.keysym;
  11340                    
  11341                    // Send key event only if keysym is meaningful
  11342                    if (key.keysym && osk.onkeydown)
  11343                        osk.onkeydown(key.keysym);
  11344
  11345                }
  11346
  11347                // Deactivate if not pressed
  11348                else {
  11349
  11350                    removeClass(keyboard, modifierClass);
  11351                    delete modifierKeysyms[key.modifier];
  11352                    
  11353                    // Send key event only if original keysym is meaningful
  11354                    if (originalKeysym && osk.onkeyup)
  11355                        osk.onkeyup(originalKeysym);
  11356
  11357                }
  11358
  11359            }
  11360
  11361            // If not modifier, send key event now
  11362            else if (osk.onkeydown)
  11363                osk.onkeydown(key.keysym);
  11364
  11365            // Mark key as pressed
  11366            pressed[keyName] = true;
  11367
  11368        }
  11369
  11370    };
  11371
  11372    /**
  11373     * Releases the key having the given name, removing the
  11374     * "guac-keyboard-pressed" CSS class from the associated element. If the
  11375     * key is already released, this function has no effect.
  11376     *
  11377     * @private
  11378     * @param {!string} keyName
  11379     *     The name of the key to release.
  11380     *
  11381     * @param {!string} keyElement
  11382     *     The element associated with the given key.
  11383     */
  11384    var release = function release(keyName, keyElement) {
  11385
  11386        // Release key if currently pressed
  11387        if (pressed[keyName]) {
  11388
  11389            removeClass(keyElement, "guac-keyboard-pressed");
  11390
  11391            // Get current key based on modifier state
  11392            var key = getActiveKey(keyName);
  11393
  11394            // Send key event if not a modifier key
  11395            if (!key.modifier && osk.onkeyup)
  11396                osk.onkeyup(key.keysym);
  11397
  11398            // Mark key as released
  11399            pressed[keyName] = false;
  11400
  11401        }
  11402
  11403    };
  11404
  11405    // Create keyboard
  11406    var keyboard = document.createElement("div");
  11407    keyboard.className = "guac-keyboard";
  11408
  11409    // Do not allow selection or mouse movement to propagate/register.
  11410    keyboard.onselectstart =
  11411    keyboard.onmousemove   =
  11412    keyboard.onmouseup     =
  11413    keyboard.onmousedown   = function handleMouseEvents(e) {
  11414
  11415        // If ignoring events, decrement counter
  11416        if (ignoreMouse)
  11417            ignoreMouse--;
  11418
  11419        e.stopPropagation();
  11420        return false;
  11421
  11422    };
  11423
  11424    /**
  11425     * The number of mousemove events to require before re-enabling mouse
  11426     * event handling after receiving a touch event.
  11427     *
  11428     * @type {!number}
  11429     */
  11430    this.touchMouseThreshold = 3;
  11431
  11432    /**
  11433     * Fired whenever the user presses a key on this Guacamole.OnScreenKeyboard.
  11434     * 
  11435     * @event
  11436     * @param {!number} keysym
  11437     *     The keysym of the key being pressed.
  11438     */
  11439    this.onkeydown = null;
  11440
  11441    /**
  11442     * Fired whenever the user releases a key on this Guacamole.OnScreenKeyboard.
  11443     * 
  11444     * @event
  11445     * @param {!number} keysym
  11446     *     The keysym of the key being released.
  11447     */
  11448    this.onkeyup = null;
  11449
  11450    /**
  11451     * The keyboard layout provided at time of construction.
  11452     *
  11453     * @type {!Guacamole.OnScreenKeyboard.Layout}
  11454     */
  11455    this.layout = new Guacamole.OnScreenKeyboard.Layout(layout);
  11456
  11457    /**
  11458     * Returns the element containing the entire on-screen keyboard.
  11459     *
  11460     * @returns {!Element}
  11461     *     The element containing the entire on-screen keyboard.
  11462     */
  11463    this.getElement = function() {
  11464        return keyboard;
  11465    };
  11466
  11467    /**
  11468     * Resizes all elements within this Guacamole.OnScreenKeyboard such that
  11469     * the width is close to but does not exceed the specified width. The
  11470     * height of the keyboard is determined based on the width.
  11471     * 
  11472     * @param {!number} width
  11473     *     The width to resize this Guacamole.OnScreenKeyboard to, in pixels.
  11474     */
  11475    this.resize = function(width) {
  11476
  11477        // Get pixel size of a unit
  11478        var unit = Math.floor(width * 10 / osk.layout.width) / 10;
  11479
  11480        // Resize all scaled elements
  11481        for (var i=0; i<scaledElements.length; i++) {
  11482            var scaledElement = scaledElements[i];
  11483            scaledElement.scale(unit);
  11484        }
  11485
  11486    };
  11487
  11488    /**
  11489     * Given the name of a key and its corresponding definition, which may be
  11490     * an array of keys objects, a number (keysym), a string (key title), or a
  11491     * single key object, returns an array of key objects, deriving any missing
  11492     * properties as needed, and ensuring the key name is defined.
  11493     *
  11494     * @private
  11495     * @param {!string} name
  11496     *     The name of the key being coerced into an array of Key objects.
  11497     *
  11498     * @param {!(number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[])} object
  11499     *     The object defining the behavior of the key having the given name,
  11500     *     which may be the title of the key (a string), the keysym (a number),
  11501     *     a single Key object, or an array of Key objects.
  11502     *     
  11503     * @returns {!Guacamole.OnScreenKeyboard.Key[]}
  11504     *     An array of all keys associated with the given name.
  11505     */
  11506    var asKeyArray = function asKeyArray(name, object) {
  11507
  11508        // If already an array, just coerce into a true Key[] 
  11509        if (object instanceof Array) {
  11510            var keys = [];
  11511            for (var i=0; i < object.length; i++) {
  11512                keys.push(new Guacamole.OnScreenKeyboard.Key(object[i], name));
  11513            }
  11514            return keys;
  11515        }
  11516
  11517        // Derive key object from keysym if that's all we have
  11518        if (typeof object === 'number') {
  11519            return [new Guacamole.OnScreenKeyboard.Key({
  11520                name   : name,
  11521                keysym : object
  11522            })];
  11523        }
  11524
  11525        // Derive key object from title if that's all we have
  11526        if (typeof object === 'string') {
  11527            return [new Guacamole.OnScreenKeyboard.Key({
  11528                name  : name,
  11529                title : object
  11530            })];
  11531        }
  11532
  11533        // Otherwise, assume it's already a key object, just not an array
  11534        return [new Guacamole.OnScreenKeyboard.Key(object, name)];
  11535
  11536    };
  11537
  11538    /**
  11539     * Converts the rather forgiving key mapping allowed by
  11540     * Guacamole.OnScreenKeyboard.Layout into a rigorous mapping of key name
  11541     * to key definition, where the key definition is always an array of Key
  11542     * objects.
  11543     *
  11544     * @private
  11545     * @param {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>} keys
  11546     *     A mapping of key name to key definition, where the key definition is
  11547     *     the title of the key (a string), the keysym (a number), a single
  11548     *     Key object, or an array of Key objects.
  11549     *
  11550     * @returns {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
  11551     *     A more-predictable mapping of key name to key definition, where the
  11552     *     key definition is always simply an array of Key objects.
  11553     */
  11554    var getKeys = function getKeys(keys) {
  11555
  11556        var keyArrays = {};
  11557
  11558        // Coerce all keys into individual key arrays
  11559        for (var name in layout.keys) {
  11560            keyArrays[name] = asKeyArray(name, keys[name]);
  11561        }
  11562
  11563        return keyArrays;
  11564
  11565    };
  11566
  11567    /**
  11568     * Map of all key names to their corresponding set of keys. Each key name
  11569     * may correspond to multiple keys due to the effect of modifiers.
  11570     *
  11571     * @type {!Object.<string, Guacamole.OnScreenKeyboard.Key[]>}
  11572     */
  11573    this.keys = getKeys(layout.keys);
  11574
  11575    /**
  11576     * Given an arbitrary string representing the name of some component of the
  11577     * on-screen keyboard, returns a string formatted for use as a CSS class
  11578     * name. The result will be lowercase. Word boundaries previously denoted
  11579     * by CamelCase will be replaced by individual hyphens, as will all
  11580     * contiguous non-alphanumeric characters.
  11581     *
  11582     * @private
  11583     * @param {!string} name
  11584     *     An arbitrary string representing the name of some component of the
  11585     *     on-screen keyboard.
  11586     *
  11587     * @returns {!string}
  11588     *     A string formatted for use as a CSS class name.
  11589     */
  11590    var getCSSName = function getCSSName(name) {
  11591
  11592        // Convert name from possibly-CamelCase to hyphenated lowercase
  11593        var cssName = name
  11594               .replace(/([a-z])([A-Z])/g, '$1-$2')
  11595               .replace(/[^A-Za-z0-9]+/g, '-')
  11596               .toLowerCase();
  11597
  11598        return cssName;
  11599
  11600    };
  11601
  11602    /**
  11603     * Appends DOM elements to the given element as dictated by the layout
  11604     * structure object provided. If a name is provided, an additional CSS
  11605     * class, prepended with "guac-keyboard-", will be added to the top-level
  11606     * element.
  11607     * 
  11608     * If the layout structure object is an array, all elements within that
  11609     * array will be recursively appended as children of a group, and the
  11610     * top-level element will be given the CSS class "guac-keyboard-group".
  11611     *
  11612     * If the layout structure object is an object, all properties within that
  11613     * object will be recursively appended as children of a group, and the
  11614     * top-level element will be given the CSS class "guac-keyboard-group". The
  11615     * name of each property will be applied as the name of each child object
  11616     * for the sake of CSS. Each property will be added in sorted order.
  11617     *
  11618     * If the layout structure object is a string, the key having that name
  11619     * will be appended. The key will be given the CSS class
  11620     * "guac-keyboard-key" and "guac-keyboard-key-NAME", where NAME is the name
  11621     * of the key. If the name of the key is a single character, this will
  11622     * first be transformed into the C-style hexadecimal literal for the
  11623     * Unicode codepoint of that character. For example, the key "A" would
  11624     * become "guac-keyboard-key-0x41".
  11625     * 
  11626     * If the layout structure object is a number, a gap of that size will be
  11627     * inserted. The gap will be given the CSS class "guac-keyboard-gap", and
  11628     * will be scaled according to the same size units as each key.
  11629     *
  11630     * @private
  11631     * @param {!Element} element
  11632     *     The element to append elements to.
  11633     *
  11634     * @param {!(Array|object|string|number)} object
  11635     *     The layout structure object to use when constructing the elements to
  11636     *     append.
  11637     *
  11638     * @param {string} [name]
  11639     *     The name of the top-level element being appended, if any.
  11640     */
  11641    var appendElements = function appendElements(element, object, name) {
  11642
  11643        var i;
  11644
  11645        // Create div which will become the group or key
  11646        var div = document.createElement('div');
  11647
  11648        // Add class based on name, if name given
  11649        if (name)
  11650            addClass(div, 'guac-keyboard-' + getCSSName(name));
  11651
  11652        // If an array, append each element
  11653        if (object instanceof Array) {
  11654
  11655            // Add group class
  11656            addClass(div, 'guac-keyboard-group');
  11657
  11658            // Append all elements of array
  11659            for (i=0; i < object.length; i++)
  11660                appendElements(div, object[i]);
  11661
  11662        }
  11663
  11664        // If an object, append each property value
  11665        else if (object instanceof Object) {
  11666
  11667            // Add group class
  11668            addClass(div, 'guac-keyboard-group');
  11669
  11670            // Append all children, sorted by name
  11671            var names = Object.keys(object).sort();
  11672            for (i=0; i < names.length; i++) {
  11673                var name = names[i];
  11674                appendElements(div, object[name], name);
  11675            }
  11676
  11677        }
  11678
  11679        // If a number, create as a gap 
  11680        else if (typeof object === 'number') {
  11681
  11682            // Add gap class
  11683            addClass(div, 'guac-keyboard-gap');
  11684
  11685            // Maintain scale
  11686            scaledElements.push(new ScaledElement(div, object, object));
  11687
  11688        }
  11689
  11690        // If a string, create as a key
  11691        else if (typeof object === 'string') {
  11692
  11693            // If key name is only one character, use codepoint for name
  11694            var keyName = object;
  11695            if (keyName.length === 1)
  11696                keyName = '0x' + keyName.charCodeAt(0).toString(16);
  11697
  11698            // Add key container class
  11699            addClass(div, 'guac-keyboard-key-container');
  11700
  11701            // Create key element which will contain all possible caps
  11702            var keyElement = document.createElement('div');
  11703            keyElement.className = 'guac-keyboard-key '
  11704                                 + 'guac-keyboard-key-' + getCSSName(keyName);
  11705
  11706            // Add all associated keys as caps within DOM
  11707            var keys = osk.keys[object];
  11708            if (keys) {
  11709                for (i=0; i < keys.length; i++) {
  11710
  11711                    // Get current key
  11712                    var key = keys[i];
  11713
  11714                    // Create cap element for key
  11715                    var capElement = document.createElement('div');
  11716                    capElement.className   = 'guac-keyboard-cap';
  11717                    capElement.textContent = key.title;
  11718
  11719                    // Add classes for any requirements
  11720                    for (var j=0; j < key.requires.length; j++) {
  11721                        var requirement = key.requires[j];
  11722                        addClass(capElement, 'guac-keyboard-requires-' + getCSSName(requirement));
  11723                        addClass(keyElement, 'guac-keyboard-uses-'     + getCSSName(requirement));
  11724                    }
  11725
  11726                    // Add cap to key within DOM
  11727                    keyElement.appendChild(capElement);
  11728
  11729                }
  11730            }
  11731
  11732            // Add key to DOM, maintain scale
  11733            div.appendChild(keyElement);
  11734            scaledElements.push(new ScaledElement(div, osk.layout.keyWidths[object] || 1, 1, true));
  11735
  11736            /**
  11737             * Handles a touch event which results in the pressing of an OSK
  11738             * key. Touch events will result in mouse events being ignored for
  11739             * touchMouseThreshold events.
  11740             *
  11741             * @private
  11742             * @param {!TouchEvent} e
  11743             *     The touch event being handled.
  11744             */
  11745            var touchPress = function touchPress(e) {
  11746                e.preventDefault();
  11747                ignoreMouse = osk.touchMouseThreshold;
  11748                press(object, keyElement);
  11749            };
  11750
  11751            /**
  11752             * Handles a touch event which results in the release of an OSK
  11753             * key. Touch events will result in mouse events being ignored for
  11754             * touchMouseThreshold events.
  11755             *
  11756             * @private
  11757             * @param {!TouchEvent} e
  11758             *     The touch event being handled.
  11759             */
  11760            var touchRelease = function touchRelease(e) {
  11761                e.preventDefault();
  11762                ignoreMouse = osk.touchMouseThreshold;
  11763                release(object, keyElement);
  11764            };
  11765
  11766            /**
  11767             * Handles a mouse event which results in the pressing of an OSK
  11768             * key. If mouse events are currently being ignored, this handler
  11769             * does nothing.
  11770             *
  11771             * @private
  11772             * @param {!MouseEvent} e
  11773             *     The touch event being handled.
  11774             */
  11775            var mousePress = function mousePress(e) {
  11776                e.preventDefault();
  11777                if (ignoreMouse === 0)
  11778                    press(object, keyElement);
  11779            };
  11780
  11781            /**
  11782             * Handles a mouse event which results in the release of an OSK
  11783             * key. If mouse events are currently being ignored, this handler
  11784             * does nothing.
  11785             *
  11786             * @private
  11787             * @param {!MouseEvent} e
  11788             *     The touch event being handled.
  11789             */
  11790            var mouseRelease = function mouseRelease(e) {
  11791                e.preventDefault();
  11792                if (ignoreMouse === 0)
  11793                    release(object, keyElement);
  11794            };
  11795
  11796            // Handle touch events on key
  11797            keyElement.addEventListener("touchstart", touchPress,   true);
  11798            keyElement.addEventListener("touchend",   touchRelease, true);
  11799
  11800            // Handle mouse events on key
  11801            keyElement.addEventListener("mousedown", mousePress,   true);
  11802            keyElement.addEventListener("mouseup",   mouseRelease, true);
  11803            keyElement.addEventListener("mouseout",  mouseRelease, true);
  11804
  11805        } // end if object is key name
  11806
  11807        // Add newly-created group/key
  11808        element.appendChild(div);
  11809
  11810    };
  11811
  11812    // Create keyboard layout in DOM
  11813    appendElements(keyboard, layout.layout);
  11814
  11815};
  11816
  11817/**
  11818 * Represents an entire on-screen keyboard layout, including all available
  11819 * keys, their behaviors, and their relative position and sizing.
  11820 *
  11821 * @constructor
  11822 * @param {!(Guacamole.OnScreenKeyboard.Layout|object)} template
  11823 *     The object whose identically-named properties will be used to initialize
  11824 *     the properties of this layout.
  11825 */
  11826Guacamole.OnScreenKeyboard.Layout = function(template) {
  11827
  11828    /**
  11829     * The language of keyboard layout, such as "en_US". This property is for
  11830     * informational purposes only, but it is recommend to conform to the
  11831     * [language code]_[country code] format.
  11832     *
  11833     * @type {!string}
  11834     */
  11835    this.language = template.language;
  11836
  11837    /**
  11838     * The type of keyboard layout, such as "qwerty". This property is for
  11839     * informational purposes only, and does not conform to any standard.
  11840     *
  11841     * @type {!string}
  11842     */
  11843    this.type = template.type;
  11844
  11845    /**
  11846     * Map of key name to corresponding keysym, title, or key object. If only
  11847     * the keysym or title is provided, the key object will be created
  11848     * implicitly. In all cases, the name property of the key object will be
  11849     * taken from the name given in the mapping.
  11850     *
  11851     * @type {!Object.<string, number|string|Guacamole.OnScreenKeyboard.Key|Guacamole.OnScreenKeyboard.Key[]>}
  11852     */
  11853    this.keys = template.keys;
  11854
  11855    /**
  11856     * Arbitrarily nested, arbitrarily grouped key names. The contents of the
  11857     * layout will be traversed to produce an identically-nested grouping of
  11858     * keys in the DOM tree. All strings will be transformed into their
  11859     * corresponding sets of keys, while all objects and arrays will be
  11860     * transformed into named groups and anonymous groups respectively. Any
  11861     * numbers present will be transformed into gaps of that size, scaled
  11862     * according to the same units as each key.
  11863     *
  11864     * @type {!object}
  11865     */
  11866    this.layout = template.layout;
  11867
  11868    /**
  11869     * The width of the entire keyboard, in arbitrary units. The width of each
  11870     * key is relative to this width, as both width values are assumed to be in
  11871     * the same units. The conversion factor between these units and pixels is
  11872     * derived later via a call to resize() on the Guacamole.OnScreenKeyboard.
  11873     *
  11874     * @type {!number}
  11875     */
  11876    this.width = template.width;
  11877
  11878    /**
  11879     * The width of each key, in arbitrary units, relative to other keys in
  11880     * this layout. The true pixel size of each key will be determined by the
  11881     * overall size of the keyboard. If not defined here, the width of each
  11882     * key will default to 1.
  11883     *
  11884     * @type {!Object.<string, number>}
  11885     */
  11886    this.keyWidths = template.keyWidths || {};
  11887
  11888};
  11889
  11890/**
  11891 * Represents a single key, or a single possible behavior of a key. Each key
  11892 * on the on-screen keyboard must have at least one associated
  11893 * Guacamole.OnScreenKeyboard.Key, whether that key is explicitly defined or
  11894 * implied, and may have multiple Guacamole.OnScreenKeyboard.Key if behavior
  11895 * depends on modifier states.
  11896 *
  11897 * @constructor
  11898 * @param {!(Guacamole.OnScreenKeyboard.Key|object)} template
  11899 *     The object whose identically-named properties will be used to initialize
  11900 *     the properties of this key.
  11901 *     
  11902 * @param {string} [name]
  11903 *     The name to use instead of any name provided within the template, if
  11904 *     any. If omitted, the name within the template will be used, assuming the
  11905 *     template contains a name.
  11906 */
  11907Guacamole.OnScreenKeyboard.Key = function(template, name) {
  11908
  11909    /**
  11910     * The unique name identifying this key within the keyboard layout.
  11911     *
  11912     * @type {!string}
  11913     */
  11914    this.name = name || template.name;
  11915
  11916    /**
  11917     * The human-readable title that will be displayed to the user within the
  11918     * key. If not provided, this will be derived from the key name.
  11919     *
  11920     * @type {!string}
  11921     */
  11922    this.title = template.title || this.name;
  11923
  11924    /**
  11925     * The keysym to be pressed/released when this key is pressed/released. If
  11926     * not provided, this will be derived from the title if the title is a
  11927     * single character.
  11928     *
  11929     * @type {number}
  11930     */
  11931    this.keysym = template.keysym || (function deriveKeysym(title) {
  11932
  11933        // Do not derive keysym if title is not exactly one character
  11934        if (!title || title.length !== 1)
  11935            return null;
  11936
  11937        // For characters between U+0000 and U+00FF, the keysym is the codepoint
  11938        var charCode = title.charCodeAt(0);
  11939        if (charCode >= 0x0000 && charCode <= 0x00FF)
  11940            return charCode;
  11941
  11942        // For characters between U+0100 and U+10FFFF, the keysym is the codepoint or'd with 0x01000000
  11943        if (charCode >= 0x0100 && charCode <= 0x10FFFF)
  11944            return 0x01000000 | charCode;
  11945
  11946        // Unable to derive keysym
  11947        return null;
  11948
  11949    })(this.title);
  11950
  11951    /**
  11952     * The name of the modifier set when the key is pressed and cleared when
  11953     * this key is released, if any. The names of modifiers are distinct from
  11954     * the names of keys; both the "RightShift" and "LeftShift" keys may set
  11955     * the "shift" modifier, for example. By default, the key will affect no
  11956     * modifiers.
  11957     * 
  11958     * @type {string}
  11959     */
  11960    this.modifier = template.modifier;
  11961
  11962    /**
  11963     * An array containing the names of each modifier required for this key to
  11964     * have an effect. For example, a lowercase letter may require nothing,
  11965     * while an uppercase letter would require "shift", assuming the Shift key
  11966     * is named "shift" within the layout. By default, the key will require
  11967     * no modifiers.
  11968     *
  11969     * @type {!string[]}
  11970     */
  11971    this.requires = template.requires || [];
  11972
  11973};
  11974/*
  11975 * Licensed to the Apache Software Foundation (ASF) under one
  11976 * or more contributor license agreements.  See the NOTICE file
  11977 * distributed with this work for additional information
  11978 * regarding copyright ownership.  The ASF licenses this file
  11979 * to you under the Apache License, Version 2.0 (the
  11980 * "License"); you may not use this file except in compliance
  11981 * with the License.  You may obtain a copy of the License at
  11982 *
  11983 *   http://www.apache.org/licenses/LICENSE-2.0
  11984 *
  11985 * Unless required by applicable law or agreed to in writing,
  11986 * software distributed under the License is distributed on an
  11987 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  11988 * KIND, either express or implied.  See the License for the
  11989 * specific language governing permissions and limitations
  11990 * under the License.
  11991 */
  11992
  11993var Guacamole = Guacamole || {};
  11994
  11995/**
  11996 * Abstract stream which can receive data.
  11997 * 
  11998 * @constructor
  11999 * @param {!Guacamole.Client} client
  12000 *     The client owning this stream.
  12001 *
  12002 * @param {!number} index
  12003 *     The index of this stream.
  12004 */
  12005Guacamole.OutputStream = function(client, index) {
  12006
  12007    /**
  12008     * Reference to this stream.
  12009     *
  12010     * @private
  12011     * @type {!Guacamole.OutputStream}
  12012     */
  12013    var guac_stream = this;
  12014
  12015    /**
  12016     * The index of this stream.
  12017     * @type {!number}
  12018     */
  12019    this.index = index;
  12020
  12021    /**
  12022     * Fired whenever an acknowledgement is received from the server, indicating
  12023     * that a stream operation has completed, or an error has occurred.
  12024     * 
  12025     * @event
  12026     * @param {!Guacamole.Status} status
  12027     *     The status of the operation.
  12028     */
  12029    this.onack = null;
  12030
  12031    /**
  12032     * Writes the given base64-encoded data to this stream as a blob.
  12033     * 
  12034     * @param {!string} data
  12035     *     The base64-encoded data to send.
  12036     */
  12037    this.sendBlob = function(data) {
  12038        client.sendBlob(guac_stream.index, data);
  12039    };
  12040
  12041    /**
  12042     * Closes this stream.
  12043     */
  12044    this.sendEnd = function() {
  12045        client.endStream(guac_stream.index);
  12046    };
  12047
  12048};
  12049/*
  12050 * Licensed to the Apache Software Foundation (ASF) under one
  12051 * or more contributor license agreements.  See the NOTICE file
  12052 * distributed with this work for additional information
  12053 * regarding copyright ownership.  The ASF licenses this file
  12054 * to you under the Apache License, Version 2.0 (the
  12055 * "License"); you may not use this file except in compliance
  12056 * with the License.  You may obtain a copy of the License at
  12057 *
  12058 *   http://www.apache.org/licenses/LICENSE-2.0
  12059 *
  12060 * Unless required by applicable law or agreed to in writing,
  12061 * software distributed under the License is distributed on an
  12062 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  12063 * KIND, either express or implied.  See the License for the
  12064 * specific language governing permissions and limitations
  12065 * under the License.
  12066 */
  12067
  12068var Guacamole = Guacamole || {};
  12069
  12070/**
  12071 * Simple Guacamole protocol parser that invokes an oninstruction event when
  12072 * full instructions are available from data received via receive().
  12073 * 
  12074 * @constructor
  12075 */
  12076Guacamole.Parser = function() {
  12077
  12078    /**
  12079     * Reference to this parser.
  12080     * @private
  12081     */
  12082    var parser = this;
  12083
  12084    /**
  12085     * Current buffer of received data. This buffer grows until a full
  12086     * element is available. After a full element is available, that element
  12087     * is flushed into the element buffer.
  12088     * 
  12089     * @private
  12090     */
  12091    var buffer = "";
  12092
  12093    /**
  12094     * Buffer of all received, complete elements. After an entire instruction
  12095     * is read, this buffer is flushed, and a new instruction begins.
  12096     * 
  12097     * @private
  12098     */
  12099    var element_buffer = [];
  12100
  12101    // The location of the last element's terminator
  12102    var element_end = -1;
  12103
  12104    // Where to start the next length search or the next element
  12105    var start_index = 0;
  12106
  12107    /**
  12108     * Appends the given instruction data packet to the internal buffer of
  12109     * this Guacamole.Parser, executing all completed instructions at
  12110     * the beginning of this buffer, if any.
  12111     *
  12112     * @param {!string} packet
  12113     *     The instruction data to receive.
  12114     */
  12115    this.receive = function(packet) {
  12116
  12117        // Truncate buffer as necessary
  12118        if (start_index > 4096 && element_end >= start_index) {
  12119
  12120            buffer = buffer.substring(start_index);
  12121
  12122            // Reset parse relative to truncation
  12123            element_end -= start_index;
  12124            start_index = 0;
  12125
  12126        }
  12127
  12128        // Append data to buffer
  12129        buffer += packet;
  12130
  12131        // While search is within currently received data
  12132        while (element_end < buffer.length) {
  12133
  12134            // If we are waiting for element data
  12135            if (element_end >= start_index) {
  12136
  12137                // We now have enough data for the element. Parse.
  12138                var element = buffer.substring(start_index, element_end);
  12139                var terminator = buffer.substring(element_end, element_end+1);
  12140
  12141                // Add element to array
  12142                element_buffer.push(element);
  12143
  12144                // If last element, handle instruction
  12145                if (terminator == ";") {
  12146
  12147                    // Get opcode
  12148                    var opcode = element_buffer.shift();
  12149
  12150                    // Call instruction handler.
  12151                    if (parser.oninstruction != null)
  12152                        parser.oninstruction(opcode, element_buffer);
  12153
  12154                    // Clear elements
  12155                    element_buffer.length = 0;
  12156
  12157                }
  12158                else if (terminator != ',')
  12159                    throw new Error("Illegal terminator.");
  12160
  12161                // Start searching for length at character after
  12162                // element terminator
  12163                start_index = element_end + 1;
  12164
  12165            }
  12166
  12167            // Search for end of length
  12168            var length_end = buffer.indexOf(".", start_index);
  12169            if (length_end != -1) {
  12170
  12171                // Parse length
  12172                var length = parseInt(buffer.substring(element_end+1, length_end));
  12173                if (isNaN(length))
  12174                    throw new Error("Non-numeric character in element length.");
  12175
  12176                // Calculate start of element
  12177                start_index = length_end + 1;
  12178
  12179                // Calculate location of element terminator
  12180                element_end = start_index + length;
  12181
  12182            }
  12183            
  12184            // If no period yet, continue search when more data
  12185            // is received
  12186            else {
  12187                start_index = buffer.length;
  12188                break;
  12189            }
  12190
  12191        } // end parse loop
  12192
  12193    };
  12194
  12195    /**
  12196     * Fired once for every complete Guacamole instruction received, in order.
  12197     * 
  12198     * @event
  12199     * @param {!string} opcode
  12200     *     The Guacamole instruction opcode.
  12201     *
  12202     * @param {!string[]} parameters
  12203     *     The parameters provided for the instruction, if any.
  12204     */
  12205    this.oninstruction = null;
  12206
  12207};
  12208/*
  12209 * Licensed to the Apache Software Foundation (ASF) under one
  12210 * or more contributor license agreements.  See the NOTICE file
  12211 * distributed with this work for additional information
  12212 * regarding copyright ownership.  The ASF licenses this file
  12213 * to you under the Apache License, Version 2.0 (the
  12214 * "License"); you may not use this file except in compliance
  12215 * with the License.  You may obtain a copy of the License at
  12216 *
  12217 *   http://www.apache.org/licenses/LICENSE-2.0
  12218 *
  12219 * Unless required by applicable law or agreed to in writing,
  12220 * software distributed under the License is distributed on an
  12221 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  12222 * KIND, either express or implied.  See the License for the
  12223 * specific language governing permissions and limitations
  12224 * under the License.
  12225 */
  12226
  12227var Guacamole = Guacamole || {};
  12228
  12229/**
  12230 * A position in 2-D space.
  12231 *
  12232 * @constructor
  12233 * @param {Guacamole.Position|object} [template={}]
  12234 *     The object whose properties should be copied within the new
  12235 *     Guacamole.Position.
  12236 */
  12237Guacamole.Position = function Position(template) {
  12238
  12239    template = template || {};
  12240
  12241    /**
  12242     * The current X position, in pixels.
  12243     *
  12244     * @type {!number}
  12245     * @default 0
  12246     */
  12247    this.x = template.x || 0;
  12248
  12249    /**
  12250     * The current Y position, in pixels.
  12251     *
  12252     * @type {!number}
  12253     * @default 0
  12254     */
  12255    this.y = template.y || 0;
  12256
  12257    /**
  12258     * Assigns the position represented by the given element and
  12259     * clientX/clientY coordinates. The clientX and clientY coordinates are
  12260     * relative to the browser viewport and are commonly available within
  12261     * JavaScript event objects. The final position is translated to
  12262     * coordinates that are relative the given element.
  12263     *
  12264     * @param {!Element} element
  12265     *     The element the coordinates should be relative to.
  12266     *
  12267     * @param {!number} clientX
  12268     *     The viewport-relative X coordinate to translate.
  12269     *
  12270     * @param {!number} clientY
  12271     *     The viewport-relative Y coordinate to translate.
  12272     */
  12273    this.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
  12274
  12275        this.x = clientX - element.offsetLeft;
  12276        this.y = clientY - element.offsetTop;
  12277
  12278        // This is all JUST so we can get the position within the element
  12279        var parent = element.offsetParent;
  12280        while (parent && !(parent === document.body)) {
  12281            this.x -= parent.offsetLeft - parent.scrollLeft;
  12282            this.y -= parent.offsetTop  - parent.scrollTop;
  12283
  12284            parent = parent.offsetParent;
  12285        }
  12286
  12287        // Element ultimately depends on positioning within document body,
  12288        // take document scroll into account.
  12289        if (parent) {
  12290            var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
  12291            var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  12292
  12293            this.x -= parent.offsetLeft - documentScrollLeft;
  12294            this.y -= parent.offsetTop  - documentScrollTop;
  12295        }
  12296
  12297    };
  12298
  12299};
  12300
  12301/**
  12302 * Returns a new {@link Guacamole.Position} representing the relative position
  12303 * of the given clientX/clientY coordinates within the given element. The
  12304 * clientX and clientY coordinates are relative to the browser viewport and are
  12305 * commonly available within JavaScript event objects. The final position is
  12306 * translated to  coordinates that are relative the given element.
  12307 *
  12308 * @param {!Element} element
  12309 *     The element the coordinates should be relative to.
  12310 *
  12311 * @param {!number} clientX
  12312 *     The viewport-relative X coordinate to translate.
  12313 *
  12314 * @param {!number} clientY
  12315 *     The viewport-relative Y coordinate to translate.
  12316 *
  12317 * @returns {!Guacamole.Position}
  12318 *     A new Guacamole.Position representing the relative position of the given
  12319 *     client coordinates.
  12320 */
  12321Guacamole.Position.fromClientPosition = function fromClientPosition(element, clientX, clientY) {
  12322    var position = new Guacamole.Position();
  12323    position.fromClientPosition(element, clientX, clientY);
  12324    return position;
  12325};
  12326/*
  12327 * Licensed to the Apache Software Foundation (ASF) under one
  12328 * or more contributor license agreements.  See the NOTICE file
  12329 * distributed with this work for additional information
  12330 * regarding copyright ownership.  The ASF licenses this file
  12331 * to you under the Apache License, Version 2.0 (the
  12332 * "License"); you may not use this file except in compliance
  12333 * with the License.  You may obtain a copy of the License at
  12334 *
  12335 *   http://www.apache.org/licenses/LICENSE-2.0
  12336 *
  12337 * Unless required by applicable law or agreed to in writing,
  12338 * software distributed under the License is distributed on an
  12339 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  12340 * KIND, either express or implied.  See the License for the
  12341 * specific language governing permissions and limitations
  12342 * under the License.
  12343 */
  12344
  12345var Guacamole = Guacamole || {};
  12346
  12347/**
  12348 * A description of the format of raw PCM audio, such as that used by
  12349 * Guacamole.RawAudioPlayer and Guacamole.RawAudioRecorder. This object
  12350 * describes the number of bytes per sample, the number of channels, and the
  12351 * overall sample rate.
  12352 *
  12353 * @constructor
  12354 * @param {!(Guacamole.RawAudioFormat|object)} template
  12355 *     The object whose properties should be copied into the corresponding
  12356 *     properties of the new Guacamole.RawAudioFormat.
  12357 */
  12358Guacamole.RawAudioFormat = function RawAudioFormat(template) {
  12359
  12360    /**
  12361     * The number of bytes in each sample of audio data. This value is
  12362     * independent of the number of channels.
  12363     *
  12364     * @type {!number}
  12365     */
  12366    this.bytesPerSample = template.bytesPerSample;
  12367
  12368    /**
  12369     * The number of audio channels (ie: 1 for mono, 2 for stereo).
  12370     *
  12371     * @type {!number}
  12372     */
  12373    this.channels = template.channels;
  12374
  12375    /**
  12376     * The number of samples per second, per channel.
  12377     *
  12378     * @type {!number}
  12379     */
  12380    this.rate = template.rate;
  12381
  12382};
  12383
  12384/**
  12385 * Parses the given mimetype, returning a new Guacamole.RawAudioFormat
  12386 * which describes the type of raw audio data represented by that mimetype. If
  12387 * the mimetype is not a supported raw audio data mimetype, null is returned.
  12388 *
  12389 * @param {!string} mimetype
  12390 *     The audio mimetype to parse.
  12391 *
  12392 * @returns {Guacamole.RawAudioFormat}
  12393 *     A new Guacamole.RawAudioFormat which describes the type of raw
  12394 *     audio data represented by the given mimetype, or null if the given
  12395 *     mimetype is not supported.
  12396 */
  12397Guacamole.RawAudioFormat.parse = function parseFormat(mimetype) {
  12398
  12399    var bytesPerSample;
  12400
  12401    // Rate is absolutely required - if null is still present later, the
  12402    // mimetype must not be supported
  12403    var rate = null;
  12404
  12405    // Default for both "audio/L8" and "audio/L16" is one channel
  12406    var channels = 1;
  12407
  12408    // "audio/L8" has one byte per sample
  12409    if (mimetype.substring(0, 9) === 'audio/L8;') {
  12410        mimetype = mimetype.substring(9);
  12411        bytesPerSample = 1;
  12412    }
  12413
  12414    // "audio/L16" has two bytes per sample
  12415    else if (mimetype.substring(0, 10) === 'audio/L16;') {
  12416        mimetype = mimetype.substring(10);
  12417        bytesPerSample = 2;
  12418    }
  12419
  12420    // All other types are unsupported
  12421    else
  12422        return null;
  12423
  12424    // Parse all parameters
  12425    var parameters = mimetype.split(',');
  12426    for (var i = 0; i < parameters.length; i++) {
  12427
  12428        var parameter = parameters[i];
  12429
  12430        // All parameters must have an equals sign separating name from value
  12431        var equals = parameter.indexOf('=');
  12432        if (equals === -1)
  12433            return null;
  12434
  12435        // Parse name and value from parameter string
  12436        var name  = parameter.substring(0, equals);
  12437        var value = parameter.substring(equals+1);
  12438
  12439        // Handle each supported parameter
  12440        switch (name) {
  12441
  12442            // Number of audio channels
  12443            case 'channels':
  12444                channels = parseInt(value);
  12445                break;
  12446
  12447            // Sample rate
  12448            case 'rate':
  12449                rate = parseInt(value);
  12450                break;
  12451
  12452            // All other parameters are unsupported
  12453            default:
  12454                return null;
  12455
  12456        }
  12457
  12458    };
  12459
  12460    // The rate parameter is required
  12461    if (rate === null)
  12462        return null;
  12463
  12464    // Return parsed format details
  12465    return new Guacamole.RawAudioFormat({
  12466        bytesPerSample : bytesPerSample,
  12467        channels       : channels,
  12468        rate           : rate
  12469    });
  12470
  12471};
  12472/*
  12473 * Licensed to the Apache Software Foundation (ASF) under one
  12474 * or more contributor license agreements.  See the NOTICE file
  12475 * distributed with this work for additional information
  12476 * regarding copyright ownership.  The ASF licenses this file
  12477 * to you under the Apache License, Version 2.0 (the
  12478 * "License"); you may not use this file except in compliance
  12479 * with the License.  You may obtain a copy of the License at
  12480 *
  12481 *   http://www.apache.org/licenses/LICENSE-2.0
  12482 *
  12483 * Unless required by applicable law or agreed to in writing,
  12484 * software distributed under the License is distributed on an
  12485 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  12486 * KIND, either express or implied.  See the License for the
  12487 * specific language governing permissions and limitations
  12488 * under the License.
  12489 */
  12490
  12491var Guacamole = Guacamole || {};
  12492
  12493/**
  12494 * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} or Blob,
  12495 * the Guacamole.SessionRecording automatically parses Guacamole instructions
  12496 * within the recording source as it plays back the recording. Playback of the
  12497 * recording may be controlled through function calls to the
  12498 * Guacamole.SessionRecording, even while the recording has not yet finished
  12499 * being created or downloaded. Parsing of the contents of the recording will
  12500 * begin immediately and automatically after this constructor is invoked.
  12501 *
  12502 * @constructor
  12503 * @param {!Blob|Guacamole.Tunnel} source
  12504 *     The Blob from which the instructions of the recording should
  12505 *     be read.
  12506 */
  12507Guacamole.SessionRecording = function SessionRecording(source) {
  12508
  12509    /**
  12510     * Reference to this Guacamole.SessionRecording.
  12511     *
  12512     * @private
  12513     * @type {!Guacamole.SessionRecording}
  12514     */
  12515    var recording = this;
  12516
  12517    /**
  12518     * The Blob from which the instructions of the recording should be read.
  12519     * Note that this value is initialized far below.
  12520     *
  12521     * @private
  12522     * @type {!Blob}
  12523     */
  12524    var recordingBlob;
  12525
  12526    /**
  12527     * The tunnel from which the recording should be read, if the recording is
  12528     * being read from a tunnel. If the recording was supplied as a Blob, this
  12529     * will be null.
  12530     *
  12531     * @private
  12532     * @type {Guacamole.Tunnel}
  12533     */
  12534    var tunnel = null;
  12535
  12536    /**
  12537     * The number of bytes that this Guacamole.SessionRecording should attempt
  12538     * to read from the given blob in each read operation. Larger blocks will
  12539     * generally read the blob more quickly, but may result in excessive
  12540     * time being spent within the parser, making the page unresponsive
  12541     * while the recording is loading.
  12542     *
  12543     * @private
  12544     * @constant
  12545     * @type {Number}
  12546     */
  12547    var BLOCK_SIZE = 262144;
  12548
  12549    /**
  12550     * The minimum number of characters which must have been read between
  12551     * keyframes.
  12552     *
  12553     * @private
  12554     * @constant
  12555     * @type {Number}
  12556     */
  12557    var KEYFRAME_CHAR_INTERVAL = 16384;
  12558
  12559    /**
  12560     * The minimum number of milliseconds which must elapse between keyframes.
  12561     *
  12562     * @private
  12563     * @constant
  12564     * @type {Number}
  12565     */
  12566    var KEYFRAME_TIME_INTERVAL = 5000;
  12567
  12568    /**
  12569     * All frames parsed from the provided blob.
  12570     *
  12571     * @private
  12572     * @type {!Guacamole.SessionRecording._Frame[]}
  12573     */
  12574    var frames = [];
  12575
  12576    /**
  12577     * The timestamp of the last frame which was flagged for use as a keyframe.
  12578     * If no timestamp has yet been flagged, this will be 0.
  12579     *
  12580     * @private
  12581     * @type {!number}
  12582     */
  12583    var lastKeyframe = 0;
  12584
  12585    /**
  12586     * Tunnel which feeds arbitrary instructions to the client used by this
  12587     * Guacamole.SessionRecording for playback of the session recording.
  12588     *
  12589     * @private
  12590     * @type {!Guacamole.SessionRecording._PlaybackTunnel}
  12591     */
  12592    var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel();
  12593
  12594    /**
  12595     * Guacamole.Client instance used for visible playback of the session
  12596     * recording.
  12597     *
  12598     * @private
  12599     * @type {!Guacamole.Client}
  12600     */
  12601    var playbackClient = new Guacamole.Client(playbackTunnel);
  12602
  12603    /**
  12604     * The current frame rendered within the playback client. If no frame is
  12605     * yet rendered, this will be -1.
  12606     *
  12607     * @private
  12608     * @type {!number}
  12609     */
  12610    var currentFrame = -1;
  12611
  12612    /**
  12613     * The timestamp of the frame when playback began, in milliseconds. If
  12614     * playback is not in progress, this will be null.
  12615     *
  12616     * @private
  12617     * @type {number}
  12618     */
  12619    var startVideoTimestamp = null;
  12620
  12621    /**
  12622     * The real-world timestamp when playback began, in milliseconds. If
  12623     * playback is not in progress, this will be null.
  12624     *
  12625     * @private
  12626     * @type {number}
  12627     */
  12628    var startRealTimestamp = null;
  12629
  12630    /**
  12631     * An object containing a single "aborted" property which is set to
  12632     * true if the in-progress seek operation should be aborted. If no seek
  12633     * operation is in progress, this will be null.
  12634     *
  12635     * @private
  12636     * @type {object}
  12637     */
  12638    var activeSeek = null;
  12639
  12640    /**
  12641     * The byte offset within the recording blob of the first character of
  12642     * the first instruction of the current frame. Here, "current frame"
  12643     * refers to the frame currently being parsed when the provided
  12644     * recording is initially loading. If the recording is not being
  12645     * loaded, this value has no meaning.
  12646     *
  12647     * @private
  12648     * @type {!number}
  12649     */
  12650    var frameStart = 0;
  12651
  12652    /**
  12653     * The byte offset within the recording blob of the character which
  12654     * follows the last character of the most recently parsed instruction
  12655     * of the current frame. Here, "current frame" refers to the frame
  12656     * currently being parsed when the provided recording is initially
  12657     * loading. If the recording is not being loaded, this value has no
  12658     * meaning.
  12659     *
  12660     * @private
  12661     * @type {!number}
  12662     */
  12663    var frameEnd = 0;
  12664
  12665    /**
  12666     * Whether the initial loading process has been aborted. If the loading
  12667     * process has been aborted, no further blocks of data should be read
  12668     * from the recording.
  12669     *
  12670     * @private
  12671     * @type {!boolean}
  12672     */
  12673    var aborted = false;
  12674
  12675    /**
  12676     * The function to invoke when the seek operation initiated by a call
  12677     * to seek() is cancelled or successfully completed. If no seek
  12678     * operation is in progress, this will be null.
  12679     *
  12680     * @private
  12681     * @type {function}
  12682     */
  12683    var seekCallback = null;
  12684
  12685    /**
  12686     * Parses all Guacamole instructions within the given blob, invoking
  12687     * the provided instruction callback for each such instruction. Once
  12688     * the end of the blob has been reached (no instructions remain to be
  12689     * parsed), the provided completion callback is invoked. If a parse
  12690     * error prevents reading instructions from the blob, the onerror
  12691     * callback of the Guacamole.SessionRecording is invoked, and no further
  12692     * data is handled within the blob.
  12693     *
  12694     * @private
  12695     * @param {!Blob} blob
  12696     *     The blob to parse Guacamole instructions from.
  12697     *
  12698     * @param {function} [instructionCallback]
  12699     *     The callback to invoke for each Guacamole instruction read from
  12700     *     the given blob. This function must accept the same arguments
  12701     *     as the oninstruction handler of Guacamole.Parser.
  12702     *
  12703     * @param {function} [completionCallback]
  12704     *     The callback to invoke once all instructions have been read from
  12705     *     the given blob.
  12706     */
  12707    var parseBlob = function parseBlob(blob, instructionCallback, completionCallback) {
  12708
  12709        // Do not read any further blocks if loading has been aborted
  12710        if (aborted && blob === recordingBlob)
  12711            return;
  12712
  12713        // Prepare a parser to handle all instruction data within the blob,
  12714        // automatically invoking the provided instruction callback for all
  12715        // parsed instructions
  12716        var parser = new Guacamole.Parser();
  12717        parser.oninstruction = instructionCallback;
  12718
  12719        var offset = 0;
  12720        var reader = new FileReader();
  12721
  12722        /**
  12723         * Reads the block of data at offset bytes within the blob. If no
  12724         * such block exists, then the completion callback provided to
  12725         * parseBlob() is invoked as all data has been read.
  12726         *
  12727         * @private
  12728         */
  12729        var readNextBlock = function readNextBlock() {
  12730
  12731            // Do not read any further blocks if loading has been aborted
  12732            if (aborted && blob === recordingBlob)
  12733                return;
  12734
  12735            // Parse all instructions within the block, invoking the
  12736            // onerror handler if a parse error occurs
  12737            if (reader.readyState === 2 /* DONE */) {
  12738                try {
  12739                    parser.receive(reader.result);
  12740                }
  12741                catch (parseError) {
  12742                    if (recording.onerror) {
  12743                        recording.onerror(parseError.message);
  12744                    }
  12745                    return;
  12746                }
  12747            }
  12748
  12749            // If no data remains, the read operation is complete and no
  12750            // further blocks need to be read
  12751            if (offset >= blob.size) {
  12752                if (completionCallback)
  12753                    completionCallback();
  12754            }
  12755
  12756            // Otherwise, read the next block
  12757            else {
  12758                var block = blob.slice(offset, offset + BLOCK_SIZE);
  12759                offset += block.size;
  12760                reader.readAsText(block);
  12761            }
  12762
  12763        };
  12764
  12765        // Read blocks until the end of the given blob is reached
  12766        reader.onload = readNextBlock;
  12767        readNextBlock();
  12768
  12769    };
  12770
  12771    /**
  12772     * Calculates the size of the given Guacamole instruction element, in
  12773     * Unicode characters. The size returned includes the characters which
  12774     * make up the length, the "." separator between the length and the
  12775     * element itself, and the "," or ";" terminator which follows the
  12776     * element.
  12777     *
  12778     * @private
  12779     * @param {!string} value
  12780     *     The value of the element which has already been parsed (lacks
  12781     *     the initial length, "." separator, and "," or ";" terminator).
  12782     *
  12783     * @returns {!number}
  12784     *     The number of Unicode characters which would make up the given
  12785     *     element within a Guacamole instruction.
  12786     */
  12787    var getElementSize = function getElementSize(value) {
  12788
  12789        var valueLength = value.length;
  12790
  12791        // Calculate base size, assuming at least one digit, the "."
  12792        // separator, and the "," or ";" terminator
  12793        var protocolSize = valueLength + 3;
  12794
  12795        // Add one character for each additional digit that would occur
  12796        // in the element length prefix
  12797        while (valueLength >= 10) {
  12798            protocolSize++;
  12799            valueLength = Math.floor(valueLength / 10);
  12800        }
  12801
  12802        return protocolSize;
  12803
  12804    };
  12805
  12806    // Start playback client connected
  12807    playbackClient.connect();
  12808
  12809    // Hide cursor unless mouse position is received
  12810    playbackClient.getDisplay().showCursor(false);
  12811
  12812    /**
  12813     * Handles a newly-received instruction, whether from the main Blob or a
  12814     * tunnel, adding new frames and keyframes as necessary. Load progress is
  12815     * reported via onprogress automatically.
  12816     *
  12817     * @private
  12818     * @param {!string} opcode
  12819     *     The opcode of the instruction to handle.
  12820     *
  12821     * @param {!string[]} args
  12822     *     The arguments of the received instruction, if any.
  12823     */
  12824    var loadInstruction = function loadInstruction(opcode, args) {
  12825
  12826        // Advance end of frame by overall length of parsed instruction
  12827        frameEnd += getElementSize(opcode);
  12828        for (var i = 0; i < args.length; i++)
  12829            frameEnd += getElementSize(args[i]);
  12830
  12831        // Once a sync is received, store all instructions since the last
  12832        // frame as a new frame
  12833        if (opcode === 'sync') {
  12834
  12835            // Parse frame timestamp from sync instruction
  12836            var timestamp = parseInt(args[0]);
  12837
  12838            // Add a new frame containing the instructions read since last frame
  12839            var frame = new Guacamole.SessionRecording._Frame(timestamp, frameStart, frameEnd);
  12840            frames.push(frame);
  12841            frameStart = frameEnd;
  12842
  12843            // This frame should eventually become a keyframe if enough data
  12844            // has been processed and enough recording time has elapsed, or if
  12845            // this is the absolute first frame
  12846            if (frames.length === 1 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL
  12847                    && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) {
  12848                frame.keyframe = true;
  12849                lastKeyframe = frames.length - 1;
  12850            }
  12851
  12852            // Notify that additional content is available
  12853            if (recording.onprogress)
  12854                recording.onprogress(recording.getDuration(), frameEnd);
  12855
  12856        }
  12857
  12858    };
  12859
  12860    /**
  12861     * Notifies that the session recording has been fully loaded. If the onload
  12862     * handler has not been defined, this function has no effect.
  12863     *
  12864     * @private
  12865     */
  12866    var notifyLoaded = function notifyLoaded() {
  12867        if (recording.onload)
  12868            recording.onload();
  12869    };
  12870
  12871    // Read instructions from provided blob, extracting each frame
  12872    if (source instanceof Blob)
  12873        parseBlob(recordingBlob, loadInstruction, notifyLoaded);
  12874
  12875    // If tunnel provided instead of Blob, extract frames, etc. as instructions
  12876    // are received, buffering things into a Blob for future seeks
  12877    else {
  12878
  12879        tunnel = source;
  12880        recordingBlob = new Blob();
  12881
  12882        var errorEncountered = false;
  12883        var instructionBuffer = '';
  12884
  12885        // Read instructions from provided tunnel, extracting each frame
  12886        tunnel.oninstruction = function handleInstruction(opcode, args) {
  12887
  12888            // Reconstitute received instruction
  12889            instructionBuffer += opcode.length + '.' + opcode;
  12890            args.forEach(function appendArg(arg) {
  12891                instructionBuffer += ',' + arg.length + '.' + arg;
  12892            });
  12893            instructionBuffer += ';';
  12894
  12895            // Append to Blob (creating a new Blob in the process)
  12896            if (instructionBuffer.length >= BLOCK_SIZE) {
  12897                recordingBlob = new Blob([recordingBlob, instructionBuffer]);
  12898                instructionBuffer = '';
  12899            }
  12900
  12901            // Load parsed instruction into recording
  12902            loadInstruction(opcode, args);
  12903
  12904        };
  12905
  12906        // Report any errors encountered
  12907        tunnel.onerror = function tunnelError(status) {
  12908            errorEncountered = true;
  12909            if (recording.onerror)
  12910                recording.onerror(status.message);
  12911        };
  12912
  12913        tunnel.onstatechange = function tunnelStateChanged(state) {
  12914            if (state === Guacamole.Tunnel.State.CLOSED) {
  12915
  12916                // Append any remaining instructions
  12917                if (instructionBuffer.length) {
  12918                    recordingBlob = new Blob([recordingBlob, instructionBuffer]);
  12919                    instructionBuffer = '';
  12920                }
  12921
  12922                // Consider recording loaded if tunnel has closed without errors
  12923                if (!errorEncountered)
  12924                    notifyLoaded();
  12925            }
  12926        };
  12927
  12928    }
  12929
  12930    /**
  12931     * Converts the given absolute timestamp to a timestamp which is relative
  12932     * to the first frame in the recording.
  12933     *
  12934     * @private
  12935     * @param {!number} timestamp
  12936     *     The timestamp to convert to a relative timestamp.
  12937     *
  12938     * @returns {!number}
  12939     *     The difference in milliseconds between the given timestamp and the
  12940     *     first frame of the recording, or zero if no frames yet exist.
  12941     */
  12942    var toRelativeTimestamp = function toRelativeTimestamp(timestamp) {
  12943
  12944        // If no frames yet exist, all timestamps are zero
  12945        if (frames.length === 0)
  12946            return 0;
  12947
  12948        // Calculate timestamp relative to first frame
  12949        return timestamp - frames[0].timestamp;
  12950
  12951    };
  12952
  12953    /**
  12954     * Searches through the given region of frames for the frame having a
  12955     * relative timestamp closest to the timestamp given.
  12956     *
  12957     * @private
  12958     * @param {!number} minIndex
  12959     *     The index of the first frame in the region (the frame having the
  12960     *     smallest timestamp).
  12961     *
  12962     * @param {!number} maxIndex
  12963     *     The index of the last frame in the region (the frame having the
  12964     *     largest timestamp).
  12965     *
  12966     * @param {!number} timestamp
  12967     *     The relative timestamp to search for, where zero denotes the first
  12968     *     frame in the recording.
  12969     *
  12970     * @returns {!number}
  12971     *     The index of the frame having a relative timestamp closest to the
  12972     *     given value.
  12973     */
  12974    var findFrame = function findFrame(minIndex, maxIndex, timestamp) {
  12975
  12976        // Do not search if the region contains only one element
  12977        if (minIndex === maxIndex)
  12978            return minIndex;
  12979
  12980        // Split search region into two halves
  12981        var midIndex = Math.floor((minIndex + maxIndex) / 2);
  12982        var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);
  12983
  12984        // If timestamp is within lesser half, search again within that half
  12985        if (timestamp < midTimestamp && midIndex > minIndex)
  12986            return findFrame(minIndex, midIndex - 1, timestamp);
  12987
  12988        // If timestamp is within greater half, search again within that half
  12989        if (timestamp > midTimestamp && midIndex < maxIndex)
  12990            return findFrame(midIndex + 1, maxIndex, timestamp);
  12991
  12992        // Otherwise, we lucked out and found a frame with exactly the
  12993        // desired timestamp
  12994        return midIndex;
  12995
  12996    };
  12997
  12998    /**
  12999     * Replays the instructions associated with the given frame, sending those
  13000     * instructions to the playback client.
  13001     *
  13002     * @private
  13003     * @param {!number} index
  13004     *     The index of the frame within the frames array which should be
  13005     *     replayed.
  13006     *
  13007     * @param {function} callback
  13008     *     The callback to invoke once replay of the frame has completed.
  13009     */
  13010    var replayFrame = function replayFrame(index, callback) {
  13011
  13012        var frame = frames[index];
  13013
  13014        // Replay all instructions within the retrieved frame
  13015        parseBlob(recordingBlob.slice(frame.start, frame.end), function handleInstruction(opcode, args) {
  13016            playbackTunnel.receiveInstruction(opcode, args);
  13017        }, function replayCompleted() {
  13018
  13019            // Store client state if frame is flagged as a keyframe
  13020            if (frame.keyframe && !frame.clientState) {
  13021                playbackClient.exportState(function storeClientState(state) {
  13022                    frame.clientState = new Blob([JSON.stringify(state)]);
  13023                });
  13024            }
  13025
  13026            // Update state to correctly represent the current frame
  13027            currentFrame = index;
  13028
  13029            if (callback)
  13030                callback();
  13031
  13032        });
  13033
  13034    };
  13035
  13036    /**
  13037     * Moves the playback position to the given frame, resetting the state of
  13038     * the playback client and replaying frames as necessary. The seek
  13039     * operation will proceed asynchronously. If a seek operation is already in
  13040     * progress, that seek is first aborted. The progress of the seek operation
  13041     * can be observed through the onseek handler and the provided callback.
  13042     *
  13043     * @private
  13044     * @param {!number} index
  13045     *     The index of the frame which should become the new playback
  13046     *     position.
  13047     *
  13048     * @param {function} callback
  13049     *     The callback to invoke once the seek operation has completed.
  13050     *
  13051     * @param {number} [nextRealTimestamp]
  13052     *     The timestamp of the point in time that the given frame should be
  13053     *     displayed, as would be returned by new Date().getTime(). If omitted,
  13054     *     the frame will be displayed as soon as possible.
  13055     */
  13056    var seekToFrame = function seekToFrame(index, callback, nextRealTimestamp) {
  13057
  13058        // Abort any in-progress seek
  13059        abortSeek();
  13060
  13061        // Note that a new seek operation is in progress
  13062        var thisSeek = activeSeek = {
  13063            aborted : false
  13064        };
  13065
  13066        var startIndex = index;
  13067
  13068        // Replay any applicable incremental frames
  13069        var continueReplay = function continueReplay() {
  13070
  13071            // Notify of changes in position
  13072            if (recording.onseek && currentFrame > startIndex) {
  13073                recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp),
  13074                    currentFrame - startIndex, index - startIndex);
  13075            }
  13076
  13077            // Cancel seek if aborted
  13078            if (thisSeek.aborted)
  13079                return;
  13080
  13081            // If frames remain, replay the next frame
  13082            if (currentFrame < index)
  13083                replayFrame(currentFrame + 1, continueReplay);
  13084
  13085            // Otherwise, the seek operation is completed
  13086            else
  13087                callback();
  13088
  13089        };
  13090
  13091        // Continue replay after requested delay has elapsed, or
  13092        // immediately if no delay was requested
  13093        var continueAfterRequiredDelay = function continueAfterRequiredDelay() {
  13094            var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0;
  13095            if (delay)
  13096                window.setTimeout(continueReplay, delay);
  13097            else
  13098                continueReplay();
  13099        };
  13100
  13101        // Back up until startIndex represents current state
  13102        for (; startIndex >= 0; startIndex--) {
  13103
  13104            var frame = frames[startIndex];
  13105
  13106            // If we've reached the current frame, startIndex represents
  13107            // current state by definition
  13108            if (startIndex === currentFrame)
  13109                break;
  13110
  13111            // If frame has associated absolute state, make that frame the
  13112            // current state
  13113            if (frame.clientState) {
  13114                frame.clientState.text().then(function textReady(text) {
  13115                    playbackClient.importState(JSON.parse(text));
  13116                    currentFrame = startIndex;
  13117                    continueAfterRequiredDelay();
  13118                });
  13119                return;
  13120            }
  13121
  13122        }
  13123
  13124        continueAfterRequiredDelay();
  13125        
  13126    };
  13127
  13128    /**
  13129     * Aborts the seek operation currently in progress, if any. If no seek
  13130     * operation is in progress, this function has no effect.
  13131     *
  13132     * @private
  13133     */
  13134    var abortSeek = function abortSeek() {
  13135        if (activeSeek) {
  13136            activeSeek.aborted = true;
  13137            activeSeek = null;
  13138        }
  13139    };
  13140
  13141    /**
  13142     * Advances playback to the next frame in the frames array and schedules
  13143     * playback of the frame following that frame based on their associated
  13144     * timestamps. If no frames exist after the next frame, playback is paused.
  13145     *
  13146     * @private
  13147     */
  13148    var continuePlayback = function continuePlayback() {
  13149
  13150        // If frames remain after advancing, schedule next frame
  13151        if (currentFrame + 1 < frames.length) {
  13152
  13153            // Pull the upcoming frame
  13154            var next = frames[currentFrame + 1];
  13155
  13156            // Calculate the real timestamp corresponding to when the next
  13157            // frame begins
  13158            var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp;
  13159
  13160            // Advance to next frame after enough time has elapsed
  13161            seekToFrame(currentFrame + 1, function frameDelayElapsed() {
  13162                continuePlayback();
  13163            }, nextRealTimestamp);
  13164
  13165        }
  13166
  13167        // Otherwise stop playback
  13168        else
  13169            recording.pause();
  13170
  13171    };
  13172
  13173    /**
  13174     * Fired when loading of this recording has completed and all frames
  13175     * are available.
  13176     *
  13177     * @event
  13178     */
  13179    this.onload = null;
  13180
  13181    /**
  13182     * Fired when an error occurs which prevents the recording from being
  13183     * played back.
  13184     *
  13185     * @event
  13186     * @param {!string} message
  13187     *     A human-readable message describing the error that occurred.
  13188     */
  13189    this.onerror = null;
  13190
  13191    /**
  13192     * Fired when further loading of this recording has been explicitly
  13193     * aborted through a call to abort().
  13194     *
  13195     * @event
  13196     */
  13197    this.onabort = null;
  13198
  13199    /**
  13200     * Fired when new frames have become available while the recording is
  13201     * being downloaded.
  13202     *
  13203     * @event
  13204     * @param {!number} duration
  13205     *     The new duration of the recording, in milliseconds.
  13206     *
  13207     * @param {!number} parsedSize
  13208     *     The number of bytes that have been loaded/parsed.
  13209     */
  13210    this.onprogress = null;
  13211
  13212    /**
  13213     * Fired whenever playback of the recording has started.
  13214     *
  13215     * @event
  13216     */
  13217    this.onplay = null;
  13218
  13219    /**
  13220     * Fired whenever playback of the recording has been paused. This may
  13221     * happen when playback is explicitly paused with a call to pause(), or
  13222     * when playback is implicitly paused due to reaching the end of the
  13223     * recording.
  13224     *
  13225     * @event
  13226     */
  13227    this.onpause = null;
  13228
  13229    /**
  13230     * Fired whenever the playback position within the recording changes.
  13231     *
  13232     * @event
  13233     * @param {!number} position
  13234     *     The new position within the recording, in milliseconds.
  13235     *
  13236     * @param {!number} current
  13237     *     The number of frames that have been seeked through. If not
  13238     *     seeking through multiple frames due to a call to seek(), this
  13239     *     will be 1.
  13240     *
  13241     * @param {!number} total
  13242     *     The number of frames that are being seeked through in the
  13243     *     current seek operation. If not seeking through multiple frames
  13244     *     due to a call to seek(), this will be 1.
  13245     */
  13246    this.onseek = null;
  13247
  13248    /**
  13249     * Connects the underlying tunnel, beginning download of the Guacamole
  13250     * session. Playback of the Guacamole session cannot occur until at least
  13251     * one frame worth of instructions has been downloaded. If the underlying
  13252     * recording source is a Blob, this function has no effect.
  13253     *
  13254     * @param {string} [data]
  13255     *     The data to send to the tunnel when connecting.
  13256     */
  13257    this.connect = function connect(data) {
  13258        if (tunnel)
  13259            tunnel.connect(data);
  13260    };
  13261
  13262    /**
  13263     * Disconnects the underlying tunnel, stopping further download of the
  13264     * Guacamole session. If the underlying recording source is a Blob, this
  13265     * function has no effect.
  13266     */
  13267    this.disconnect = function disconnect() {
  13268        if (tunnel)
  13269            tunnel.disconnect();
  13270    };
  13271
  13272    /**
  13273     * Aborts the loading process, stopping further processing of the
  13274     * provided data. If the underlying recording source is a Guacamole tunnel,
  13275     * it will be disconnected.
  13276     */
  13277    this.abort = function abort() {
  13278        if (!aborted) {
  13279
  13280            aborted = true;
  13281            if (recording.onabort)
  13282                recording.onabort();
  13283
  13284            if (tunnel)
  13285                tunnel.disconnect();
  13286
  13287        }
  13288    };
  13289
  13290    /**
  13291     * Returns the underlying display of the Guacamole.Client used by this
  13292     * Guacamole.SessionRecording for playback. The display contains an Element
  13293     * which can be added to the DOM, causing the display (and thus playback of
  13294     * the recording) to become visible.
  13295     *
  13296     * @return {!Guacamole.Display}
  13297     *     The underlying display of the Guacamole.Client used by this
  13298     *     Guacamole.SessionRecording for playback.
  13299     */
  13300    this.getDisplay = function getDisplay() {
  13301        return playbackClient.getDisplay();
  13302    };
  13303
  13304    /**
  13305     * Returns whether playback is currently in progress.
  13306     *
  13307     * @returns {!boolean}
  13308     *     true if playback is currently in progress, false otherwise.
  13309     */
  13310    this.isPlaying = function isPlaying() {
  13311        return !!startVideoTimestamp;
  13312    };
  13313
  13314    /**
  13315     * Returns the current playback position within the recording, in
  13316     * milliseconds, where zero is the start of the recording.
  13317     *
  13318     * @returns {!number}
  13319     *     The current playback position within the recording, in milliseconds.
  13320     */
  13321    this.getPosition = function getPosition() {
  13322
  13323        // Position is simply zero if playback has not started at all
  13324        if (currentFrame === -1)
  13325            return 0;
  13326
  13327        // Return current position as a millisecond timestamp relative to the
  13328        // start of the recording
  13329        return toRelativeTimestamp(frames[currentFrame].timestamp);
  13330
  13331    };
  13332
  13333    /**
  13334     * Returns the duration of this recording, in milliseconds. If the
  13335     * recording is still being downloaded, this value will gradually increase.
  13336     *
  13337     * @returns {!number}
  13338     *     The duration of this recording, in milliseconds.
  13339     */
  13340    this.getDuration = function getDuration() {
  13341
  13342        // If no frames yet exist, duration is zero
  13343        if (frames.length === 0)
  13344            return 0;
  13345
  13346        // Recording duration is simply the timestamp of the last frame
  13347        return toRelativeTimestamp(frames[frames.length - 1].timestamp);
  13348
  13349    };
  13350
  13351    /**
  13352     * Begins continuous playback of the recording downloaded thus far.
  13353     * Playback of the recording will continue until pause() is invoked or
  13354     * until no further frames exist. Playback is initially paused when a
  13355     * Guacamole.SessionRecording is created, and must be explicitly started
  13356     * through a call to this function. If playback is already in progress,
  13357     * this function has no effect. If a seek operation is in progress,
  13358     * playback resumes at the current position, and the seek is aborted as if
  13359     * completed.
  13360     */
  13361    this.play = function play() {
  13362
  13363        // If playback is not already in progress and frames remain,
  13364        // begin playback
  13365        if (!recording.isPlaying() && currentFrame + 1 < frames.length) {
  13366
  13367            // Notify that playback is starting
  13368            if (recording.onplay)
  13369                recording.onplay();
  13370
  13371            // Store timestamp of playback start for relative scheduling of
  13372            // future frames
  13373            var next = frames[currentFrame + 1];
  13374            startVideoTimestamp = next.timestamp;
  13375            startRealTimestamp = new Date().getTime();
  13376
  13377            // Begin playback of video
  13378            continuePlayback();
  13379
  13380        }
  13381
  13382    };
  13383
  13384    /**
  13385     * Seeks to the given position within the recording. If the recording is
  13386     * currently being played back, playback will continue after the seek is
  13387     * performed. If the recording is currently paused, playback will be
  13388     * paused after the seek is performed. If a seek operation is already in
  13389     * progress, that seek is first aborted. The seek operation will proceed
  13390     * asynchronously.
  13391     *
  13392     * @param {!number} position
  13393     *     The position within the recording to seek to, in milliseconds.
  13394     *
  13395     * @param {function} [callback]
  13396     *     The callback to invoke once the seek operation has completed.
  13397     */
  13398    this.seek = function seek(position, callback) {
  13399
  13400        // Do not seek if no frames exist
  13401        if (frames.length === 0)
  13402            return;
  13403
  13404        // Abort active seek operation, if any
  13405        recording.cancel();
  13406
  13407        // Pause playback, preserving playback state
  13408        var originallyPlaying = recording.isPlaying();
  13409        recording.pause();
  13410
  13411        // Restore playback when seek is completed or cancelled
  13412        seekCallback = function restorePlaybackState() {
  13413
  13414            // Seek is no longer in progress
  13415            seekCallback = null;
  13416
  13417            // Restore playback state
  13418            if (originallyPlaying) {
  13419                recording.play();
  13420                originallyPlaying = null;
  13421            }
  13422
  13423            // Notify that seek has completed
  13424            if (callback)
  13425                callback();
  13426
  13427        };
  13428
  13429        // Perform seek
  13430        seekToFrame(findFrame(0, frames.length - 1, position), seekCallback);
  13431
  13432    };
  13433
  13434    /**
  13435     * Cancels the current seek operation, setting the current frame of the
  13436     * recording to wherever the seek operation was able to reach prior to
  13437     * being cancelled. If a callback was provided to seek(), that callback
  13438     * is invoked. If a seek operation is not currently underway, this
  13439     * function has no effect.
  13440     */
  13441    this.cancel = function cancel() {
  13442        if (seekCallback) {
  13443            abortSeek();
  13444            seekCallback();
  13445        }
  13446    };
  13447
  13448    /**
  13449     * Pauses playback of the recording, if playback is currently in progress.
  13450     * If playback is not in progress, this function has no effect. If a seek
  13451     * operation is in progress, the seek is aborted. Playback is initially
  13452     * paused when a Guacamole.SessionRecording is created, and must be
  13453     * explicitly started through a call to play().
  13454     */
  13455    this.pause = function pause() {
  13456
  13457        // Abort any in-progress seek / playback
  13458        abortSeek();
  13459
  13460        // Stop playback only if playback is in progress
  13461        if (recording.isPlaying()) {
  13462
  13463            // Notify that playback is stopping
  13464            if (recording.onpause)
  13465                recording.onpause();
  13466
  13467            // Playback is stopped
  13468            startVideoTimestamp = null;
  13469            startRealTimestamp = null;
  13470
  13471        }
  13472
  13473    };
  13474
  13475};
  13476
  13477/**
  13478 * A single frame of Guacamole session data. Each frame is made up of the set
  13479 * of instructions used to generate that frame, and the timestamp as dictated
  13480 * by the "sync" instruction terminating the frame. Optionally, a frame may
  13481 * also be associated with a snapshot of Guacamole client state, such that the
  13482 * frame can be rendered without replaying all previous frames.
  13483 *
  13484 * @private
  13485 * @constructor
  13486 * @param {!number} timestamp
  13487 *     The timestamp of this frame, as dictated by the "sync" instruction which
  13488 *     terminates the frame.
  13489 *
  13490 * @param {!number} start
  13491 *     The byte offset within the blob of the first character of the first
  13492 *     instruction of this frame.
  13493 *
  13494 * @param {!number} end
  13495 *     The byte offset within the blob of character which follows the last
  13496 *     character of the last instruction of this frame.
  13497 */
  13498Guacamole.SessionRecording._Frame = function _Frame(timestamp, start, end) {
  13499
  13500    /**
  13501     * Whether this frame should be used as a keyframe if possible. This value
  13502     * is purely advisory. The stored clientState must eventually be manually
  13503     * set for the frame to be used as a keyframe. By default, frames are not
  13504     * keyframes.
  13505     *
  13506     * @type {!boolean}
  13507     * @default false
  13508     */
  13509    this.keyframe = false;
  13510
  13511    /**
  13512     * The timestamp of this frame, as dictated by the "sync" instruction which
  13513     * terminates the frame.
  13514     *
  13515     * @type {!number}
  13516     */
  13517    this.timestamp = timestamp;
  13518
  13519    /**
  13520     * The byte offset within the blob of the first character of the first
  13521     * instruction of this frame.
  13522     *
  13523     * @type {!number}
  13524     */
  13525    this.start = start;
  13526
  13527    /**
  13528     * The byte offset within the blob of character which follows the last
  13529     * character of the last instruction of this frame.
  13530     *
  13531     * @type {!number}
  13532     */
  13533    this.end = end;
  13534
  13535    /**
  13536     * A snapshot of client state after this frame was rendered, as returned by
  13537     * a call to exportState(), serialized as JSON, and stored within a Blob.
  13538     * Use of Blobs here is required to ensure the browser can make use of
  13539     * larger disk-backed storage if the size of the recording is large. If no
  13540     * such snapshot has been taken, this will be null.
  13541     *
  13542     * @type {Blob}
  13543     * @default null
  13544     */
  13545    this.clientState = null;
  13546
  13547};
  13548
  13549/**
  13550 * A read-only Guacamole.Tunnel implementation which streams instructions
  13551 * received through explicit calls to its receiveInstruction() function.
  13552 *
  13553 * @private
  13554 * @constructor
  13555 * @augments {Guacamole.Tunnel}
  13556 */
  13557Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() {
  13558
  13559    /**
  13560     * Reference to this Guacamole.SessionRecording._PlaybackTunnel.
  13561     *
  13562     * @private
  13563     * @type {!Guacamole.SessionRecording._PlaybackTunnel}
  13564     */
  13565    var tunnel = this;
  13566
  13567    this.connect = function connect(data) {
  13568        // Do nothing
  13569    };
  13570
  13571    this.sendMessage = function sendMessage(elements) {
  13572        // Do nothing
  13573    };
  13574
  13575    this.disconnect = function disconnect() {
  13576        // Do nothing
  13577    };
  13578
  13579    /**
  13580     * Invokes this tunnel's oninstruction handler, notifying users of this
  13581     * tunnel (such as a Guacamole.Client instance) that an instruction has
  13582     * been received. If the oninstruction handler has not been set, this
  13583     * function has no effect.
  13584     *
  13585     * @param {!string} opcode
  13586     *     The opcode of the Guacamole instruction.
  13587     *
  13588     * @param {!string[]} args
  13589     *     All arguments associated with this Guacamole instruction.
  13590     */
  13591    this.receiveInstruction = function receiveInstruction(opcode, args) {
  13592        if (tunnel.oninstruction)
  13593            tunnel.oninstruction(opcode, args);
  13594    };
  13595
  13596};/*
  13597 * Licensed to the Apache Software Foundation (ASF) under one
  13598 * or more contributor license agreements.  See the NOTICE file
  13599 * distributed with this work for additional information
  13600 * regarding copyright ownership.  The ASF licenses this file
  13601 * to you under the Apache License, Version 2.0 (the
  13602 * "License"); you may not use this file except in compliance
  13603 * with the License.  You may obtain a copy of the License at
  13604 *
  13605 *   http://www.apache.org/licenses/LICENSE-2.0
  13606 *
  13607 * Unless required by applicable law or agreed to in writing,
  13608 * software distributed under the License is distributed on an
  13609 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13610 * KIND, either express or implied.  See the License for the
  13611 * specific language governing permissions and limitations
  13612 * under the License.
  13613 */
  13614
  13615var Guacamole = Guacamole || {};
  13616
  13617/**
  13618 * A Guacamole status. Each Guacamole status consists of a status code, defined
  13619 * by the protocol, and an optional human-readable message, usually only
  13620 * included for debugging convenience.
  13621 *
  13622 * @constructor
  13623 * @param {!number} code
  13624 *     The Guacamole status code, as defined by Guacamole.Status.Code.
  13625 *
  13626 * @param {string} [message]
  13627 *     An optional human-readable message.
  13628 */
  13629Guacamole.Status = function(code, message) {
  13630
  13631    /**
  13632     * Reference to this Guacamole.Status.
  13633     *
  13634     * @private
  13635     * @type {!Guacamole.Status}
  13636     */
  13637    var guac_status = this;
  13638
  13639    /**
  13640     * The Guacamole status code.
  13641     *
  13642     * @see Guacamole.Status.Code
  13643     * @type {!number}
  13644     */
  13645    this.code = code;
  13646
  13647    /**
  13648     * An arbitrary human-readable message associated with this status, if any.
  13649     * The human-readable message is not required, and is generally provided
  13650     * for debugging purposes only. For user feedback, it is better to translate
  13651     * the Guacamole status code into a message.
  13652     * 
  13653     * @type {string}
  13654     */
  13655    this.message = message;
  13656
  13657    /**
  13658     * Returns whether this status represents an error.
  13659     *
  13660     * @returns {!boolean}
  13661     *     true if this status represents an error, false otherwise.
  13662     */
  13663    this.isError = function() {
  13664        return guac_status.code < 0 || guac_status.code > 0x00FF;
  13665    };
  13666
  13667};
  13668
  13669/**
  13670 * Enumeration of all Guacamole status codes.
  13671 */
  13672Guacamole.Status.Code = {
  13673
  13674    /**
  13675     * The operation succeeded.
  13676     *
  13677     * @type {!number}
  13678     */
  13679    "SUCCESS": 0x0000,
  13680
  13681    /**
  13682     * The requested operation is unsupported.
  13683     *
  13684     * @type {!number}
  13685     */
  13686    "UNSUPPORTED": 0x0100,
  13687
  13688    /**
  13689     * The operation could not be performed due to an internal failure.
  13690     *
  13691     * @type {!number}
  13692     */
  13693    "SERVER_ERROR": 0x0200,
  13694
  13695    /**
  13696     * The operation could not be performed as the server is busy.
  13697     *
  13698     * @type {!number}
  13699     */
  13700    "SERVER_BUSY": 0x0201,
  13701
  13702    /**
  13703     * The operation could not be performed because the upstream server is not
  13704     * responding.
  13705     *
  13706     * @type {!number}
  13707     */
  13708    "UPSTREAM_TIMEOUT": 0x0202,
  13709
  13710    /**
  13711     * The operation was unsuccessful due to an error or otherwise unexpected
  13712     * condition of the upstream server.
  13713     *
  13714     * @type {!number}
  13715     */
  13716    "UPSTREAM_ERROR": 0x0203,
  13717
  13718    /**
  13719     * The operation could not be performed as the requested resource does not
  13720     * exist.
  13721     *
  13722     * @type {!number}
  13723     */
  13724    "RESOURCE_NOT_FOUND": 0x0204,
  13725
  13726    /**
  13727     * The operation could not be performed as the requested resource is
  13728     * already in use.
  13729     *
  13730     * @type {!number}
  13731     */
  13732    "RESOURCE_CONFLICT": 0x0205,
  13733
  13734    /**
  13735     * The operation could not be performed as the requested resource is now
  13736     * closed.
  13737     *
  13738     * @type {!number}
  13739     */
  13740    "RESOURCE_CLOSED": 0x0206,
  13741
  13742    /**
  13743     * The operation could not be performed because the upstream server does
  13744     * not appear to exist.
  13745     *
  13746     * @type {!number}
  13747     */
  13748    "UPSTREAM_NOT_FOUND": 0x0207,
  13749
  13750    /**
  13751     * The operation could not be performed because the upstream server is not
  13752     * available to service the request.
  13753     *
  13754     * @type {!number}
  13755     */
  13756    "UPSTREAM_UNAVAILABLE": 0x0208,
  13757
  13758    /**
  13759     * The session within the upstream server has ended because it conflicted
  13760     * with another session.
  13761     *
  13762     * @type {!number}
  13763     */
  13764    "SESSION_CONFLICT": 0x0209,
  13765
  13766    /**
  13767     * The session within the upstream server has ended because it appeared to
  13768     * be inactive.
  13769     *
  13770     * @type {!number}
  13771     */
  13772    "SESSION_TIMEOUT": 0x020A,
  13773
  13774    /**
  13775     * The session within the upstream server has been forcibly terminated.
  13776     *
  13777     * @type {!number}
  13778     */
  13779    "SESSION_CLOSED": 0x020B,
  13780
  13781    /**
  13782     * The operation could not be performed because bad parameters were given.
  13783     *
  13784     * @type {!number}
  13785     */
  13786    "CLIENT_BAD_REQUEST": 0x0300,
  13787
  13788    /**
  13789     * Permission was denied to perform the operation, as the user is not yet
  13790     * authorized (not yet logged in, for example).
  13791     *
  13792     * @type {!number}
  13793     */
  13794    "CLIENT_UNAUTHORIZED": 0x0301,
  13795
  13796    /**
  13797     * Permission was denied to perform the operation, and this permission will
  13798     * not be granted even if the user is authorized.
  13799     *
  13800     * @type {!number}
  13801     */
  13802    "CLIENT_FORBIDDEN": 0x0303,
  13803
  13804    /**
  13805     * The client took too long to respond.
  13806     *
  13807     * @type {!number}
  13808     */
  13809    "CLIENT_TIMEOUT": 0x0308,
  13810
  13811    /**
  13812     * The client sent too much data.
  13813     *
  13814     * @type {!number}
  13815     */
  13816    "CLIENT_OVERRUN": 0x030D,
  13817
  13818    /**
  13819     * The client sent data of an unsupported or unexpected type.
  13820     *
  13821     * @type {!number}
  13822     */
  13823    "CLIENT_BAD_TYPE": 0x030F,
  13824
  13825    /**
  13826     * The operation failed because the current client is already using too
  13827     * many resources.
  13828     *
  13829     * @type {!number}
  13830     */
  13831    "CLIENT_TOO_MANY": 0x031D
  13832
  13833};
  13834
  13835/**
  13836 * Returns the Guacamole protocol status code which most closely
  13837 * represents the given HTTP status code.
  13838 *
  13839 * @param {!number} status
  13840 *     The HTTP status code to translate into a Guacamole protocol status
  13841 *     code.
  13842 *
  13843 * @returns {!number}
  13844 *     The Guacamole protocol status code which most closely represents the
  13845 *     given HTTP status code.
  13846 */
  13847Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) {
  13848
  13849    // Translate status codes with known equivalents
  13850    switch (status) {
  13851
  13852        // HTTP 400 - Bad request
  13853        case 400:
  13854            return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
  13855
  13856        // HTTP 403 - Forbidden
  13857        case 403:
  13858            return Guacamole.Status.Code.CLIENT_FORBIDDEN;
  13859
  13860        // HTTP 404 - Resource not found
  13861        case 404:
  13862            return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
  13863
  13864        // HTTP 429 - Too many requests
  13865        case 429:
  13866            return Guacamole.Status.Code.CLIENT_TOO_MANY;
  13867
  13868        // HTTP 503 - Server unavailable
  13869        case 503:
  13870            return Guacamole.Status.Code.SERVER_BUSY;
  13871
  13872    }
  13873
  13874    // Default all other codes to generic internal error
  13875    return Guacamole.Status.Code.SERVER_ERROR;
  13876
  13877};
  13878
  13879/**
  13880 * Returns the Guacamole protocol status code which most closely
  13881 * represents the given WebSocket status code.
  13882 *
  13883 * @param {!number} code
  13884 *     The WebSocket status code to translate into a Guacamole protocol
  13885 *     status code.
  13886 *
  13887 * @returns {!number}
  13888 *     The Guacamole protocol status code which most closely represents the
  13889 *     given WebSocket status code.
  13890 */
  13891Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) {
  13892
  13893    // Translate status codes with known equivalents
  13894    switch (code) {
  13895
  13896        // Successful disconnect (no error)
  13897        case 1000: // Normal Closure
  13898            return Guacamole.Status.Code.SUCCESS;
  13899
  13900        // Codes which indicate the server is not reachable
  13901        case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place)
  13902        case 1015: // TLS Handshake
  13903            return Guacamole.Status.Code.UPSTREAM_NOT_FOUND;
  13904
  13905        // Codes which indicate the server is reachable but busy/unavailable
  13906        case 1001: // Going Away
  13907        case 1012: // Service Restart
  13908        case 1013: // Try Again Later
  13909        case 1014: // Bad Gateway
  13910            return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE;
  13911
  13912    }
  13913
  13914    // Default all other codes to generic internal error
  13915    return Guacamole.Status.Code.SERVER_ERROR;
  13916
  13917};
  13918/*
  13919 * Licensed to the Apache Software Foundation (ASF) under one
  13920 * or more contributor license agreements.  See the NOTICE file
  13921 * distributed with this work for additional information
  13922 * regarding copyright ownership.  The ASF licenses this file
  13923 * to you under the Apache License, Version 2.0 (the
  13924 * "License"); you may not use this file except in compliance
  13925 * with the License.  You may obtain a copy of the License at
  13926 *
  13927 *   http://www.apache.org/licenses/LICENSE-2.0
  13928 *
  13929 * Unless required by applicable law or agreed to in writing,
  13930 * software distributed under the License is distributed on an
  13931 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  13932 * KIND, either express or implied.  See the License for the
  13933 * specific language governing permissions and limitations
  13934 * under the License.
  13935 */
  13936
  13937var Guacamole = Guacamole || {};
  13938
  13939/**
  13940 * A reader which automatically handles the given input stream, returning
  13941 * strictly text data. Note that this object will overwrite any installed event
  13942 * handlers on the given Guacamole.InputStream.
  13943 * 
  13944 * @constructor
  13945 * @param {!Guacamole.InputStream} stream
  13946 *     The stream that data will be read from.
  13947 */
  13948Guacamole.StringReader = function(stream) {
  13949
  13950    /**
  13951     * Reference to this Guacamole.InputStream.
  13952     *
  13953     * @private
  13954     * @type {!Guacamole.StringReader}
  13955     */
  13956    var guac_reader = this;
  13957
  13958    /**
  13959     * Parser for received UTF-8 data.
  13960     *
  13961     * @type {!Guacamole.UTF8Parser}
  13962     */
  13963    var utf8Parser = new Guacamole.UTF8Parser();
  13964
  13965    /**
  13966     * Wrapped Guacamole.ArrayBufferReader.
  13967     *
  13968     * @private
  13969     * @type {!Guacamole.ArrayBufferReader}
  13970     */
  13971    var array_reader = new Guacamole.ArrayBufferReader(stream);
  13972
  13973    // Receive blobs as strings
  13974    array_reader.ondata = function(buffer) {
  13975
  13976        // Decode UTF-8
  13977        var text = utf8Parser.decode(buffer);
  13978
  13979        // Call handler, if present
  13980        if (guac_reader.ontext)
  13981            guac_reader.ontext(text);
  13982
  13983    };
  13984
  13985    // Simply call onend when end received
  13986    array_reader.onend = function() {
  13987        if (guac_reader.onend)
  13988            guac_reader.onend();
  13989    };
  13990
  13991    /**
  13992     * Fired once for every blob of text data received.
  13993     * 
  13994     * @event
  13995     * @param {!string} text
  13996     *     The data packet received.
  13997     */
  13998    this.ontext = null;
  13999
  14000    /**
  14001     * Fired once this stream is finished and no further data will be written.
  14002     * @event
  14003     */
  14004    this.onend = null;
  14005
  14006};/*
  14007 * Licensed to the Apache Software Foundation (ASF) under one
  14008 * or more contributor license agreements.  See the NOTICE file
  14009 * distributed with this work for additional information
  14010 * regarding copyright ownership.  The ASF licenses this file
  14011 * to you under the Apache License, Version 2.0 (the
  14012 * "License"); you may not use this file except in compliance
  14013 * with the License.  You may obtain a copy of the License at
  14014 *
  14015 *   http://www.apache.org/licenses/LICENSE-2.0
  14016 *
  14017 * Unless required by applicable law or agreed to in writing,
  14018 * software distributed under the License is distributed on an
  14019 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14020 * KIND, either express or implied.  See the License for the
  14021 * specific language governing permissions and limitations
  14022 * under the License.
  14023 */
  14024
  14025var Guacamole = Guacamole || {};
  14026
  14027/**
  14028 * A writer which automatically writes to the given output stream with text
  14029 * data.
  14030 * 
  14031 * @constructor
  14032 * @param {!Guacamole.OutputStream} stream
  14033 *     The stream that data will be written to.
  14034 */
  14035Guacamole.StringWriter = function(stream) {
  14036
  14037    /**
  14038     * Reference to this Guacamole.StringWriter.
  14039     *
  14040     * @private
  14041     * @type {!Guacamole.StringWriter}
  14042     */
  14043    var guac_writer = this;
  14044
  14045    /**
  14046     * Wrapped Guacamole.ArrayBufferWriter.
  14047     *
  14048     * @private
  14049     * @type {!Guacamole.ArrayBufferWriter}
  14050     */
  14051    var array_writer = new Guacamole.ArrayBufferWriter(stream);
  14052
  14053    /**
  14054     * Internal buffer for UTF-8 output.
  14055     *
  14056     * @private
  14057     * @type {!Uint8Array}
  14058     */
  14059    var buffer = new Uint8Array(8192);
  14060
  14061    /**
  14062     * The number of bytes currently in the buffer.
  14063     *
  14064     * @private
  14065     * @type {!number}
  14066     */
  14067    var length = 0;
  14068
  14069    // Simply call onack for acknowledgements
  14070    array_writer.onack = function(status) {
  14071        if (guac_writer.onack)
  14072            guac_writer.onack(status);
  14073    };
  14074
  14075    /**
  14076     * Expands the size of the underlying buffer by the given number of bytes,
  14077     * updating the length appropriately.
  14078     * 
  14079     * @private
  14080     * @param {!number} bytes
  14081     *     The number of bytes to add to the underlying buffer.
  14082     */
  14083    function __expand(bytes) {
  14084
  14085        // Resize buffer if more space needed
  14086        if (length+bytes >= buffer.length) {
  14087            var new_buffer = new Uint8Array((length+bytes)*2);
  14088            new_buffer.set(buffer);
  14089            buffer = new_buffer;
  14090        }
  14091
  14092        length += bytes;
  14093
  14094    }
  14095
  14096    /**
  14097     * Appends a single Unicode character to the current buffer, resizing the
  14098     * buffer if necessary. The character will be encoded as UTF-8.
  14099     * 
  14100     * @private
  14101     * @param {!number} codepoint
  14102     *     The codepoint of the Unicode character to append.
  14103     */
  14104    function __append_utf8(codepoint) {
  14105
  14106        var mask;
  14107        var bytes;
  14108
  14109        // 1 byte
  14110        if (codepoint <= 0x7F) {
  14111            mask = 0x00;
  14112            bytes = 1;
  14113        }
  14114
  14115        // 2 byte
  14116        else if (codepoint <= 0x7FF) {
  14117            mask = 0xC0;
  14118            bytes = 2;
  14119        }
  14120
  14121        // 3 byte
  14122        else if (codepoint <= 0xFFFF) {
  14123            mask = 0xE0;
  14124            bytes = 3;
  14125        }
  14126
  14127        // 4 byte
  14128        else if (codepoint <= 0x1FFFFF) {
  14129            mask = 0xF0;
  14130            bytes = 4;
  14131        }
  14132
  14133        // If invalid codepoint, append replacement character
  14134        else {
  14135            __append_utf8(0xFFFD);
  14136            return;
  14137        }
  14138
  14139        // Offset buffer by size
  14140        __expand(bytes);
  14141        var offset = length - 1;
  14142
  14143        // Add trailing bytes, if any
  14144        for (var i=1; i<bytes; i++) {
  14145            buffer[offset--] = 0x80 | (codepoint & 0x3F);
  14146            codepoint >>= 6;
  14147        }
  14148
  14149        // Set initial byte
  14150        buffer[offset] = mask | codepoint;
  14151
  14152    }
  14153
  14154    /**
  14155     * Encodes the given string as UTF-8, returning an ArrayBuffer containing
  14156     * the resulting bytes.
  14157     * 
  14158     * @private
  14159     * @param {!string} text
  14160     *     The string to encode as UTF-8.
  14161     *
  14162     * @return {!Uint8Array}
  14163     *     The encoded UTF-8 data.
  14164     */
  14165    function __encode_utf8(text) {
  14166
  14167        // Fill buffer with UTF-8
  14168        for (var i=0; i<text.length; i++) {
  14169            var codepoint = text.charCodeAt(i);
  14170            __append_utf8(codepoint);
  14171        }
  14172
  14173        // Flush buffer
  14174        if (length > 0) {
  14175            var out_buffer = buffer.subarray(0, length);
  14176            length = 0;
  14177            return out_buffer;
  14178        }
  14179
  14180    }
  14181
  14182    /**
  14183     * Sends the given text.
  14184     * 
  14185     * @param {!string} text
  14186     *     The text to send.
  14187     */
  14188    this.sendText = function(text) {
  14189        if (text.length)
  14190            array_writer.sendData(__encode_utf8(text));
  14191    };
  14192
  14193    /**
  14194     * Signals that no further text will be sent, effectively closing the
  14195     * stream.
  14196     */
  14197    this.sendEnd = function() {
  14198        array_writer.sendEnd();
  14199    };
  14200
  14201    /**
  14202     * Fired for received data, if acknowledged by the server.
  14203     *
  14204     * @event
  14205     * @param {!Guacamole.Status} status
  14206     *     The status of the operation.
  14207     */
  14208    this.onack = null;
  14209
  14210};/*
  14211 * Licensed to the Apache Software Foundation (ASF) under one
  14212 * or more contributor license agreements.  See the NOTICE file
  14213 * distributed with this work for additional information
  14214 * regarding copyright ownership.  The ASF licenses this file
  14215 * to you under the Apache License, Version 2.0 (the
  14216 * "License"); you may not use this file except in compliance
  14217 * with the License.  You may obtain a copy of the License at
  14218 *
  14219 *   http://www.apache.org/licenses/LICENSE-2.0
  14220 *
  14221 * Unless required by applicable law or agreed to in writing,
  14222 * software distributed under the License is distributed on an
  14223 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14224 * KIND, either express or implied.  See the License for the
  14225 * specific language governing permissions and limitations
  14226 * under the License.
  14227 */
  14228
  14229var Guacamole = Guacamole || {};
  14230
  14231/**
  14232 * Provides cross-browser multi-touch events for a given element. The events of
  14233 * the given element are automatically populated with handlers that translate
  14234 * touch events into a non-browser-specific event provided by the
  14235 * Guacamole.Touch instance.
  14236 * 
  14237 * @constructor
  14238 * @augments Guacamole.Event.Target
  14239 * @param {!Element} element
  14240 *     The Element to use to provide touch events.
  14241 */
  14242Guacamole.Touch = function Touch(element) {
  14243
  14244    Guacamole.Event.Target.call(this);
  14245
  14246    /**
  14247     * Reference to this Guacamole.Touch.
  14248     *
  14249     * @private
  14250     * @type {!Guacamole.Touch}
  14251     */
  14252    var guacTouch = this;
  14253
  14254    /**
  14255     * The default X/Y radius of each touch if the device or browser does not
  14256     * expose the size of the contact area.
  14257     *
  14258     * @private
  14259     * @constant
  14260     * @type {!number}
  14261     */
  14262    var DEFAULT_CONTACT_RADIUS = Math.floor(16 * window.devicePixelRatio);
  14263
  14264    /**
  14265     * The set of all active touches, stored by their unique identifiers.
  14266     *
  14267     * @type {!Object.<Number, Guacamole.Touch.State>}
  14268     */
  14269    this.touches = {};
  14270
  14271    /**
  14272     * The number of active touches currently stored within
  14273     * {@link Guacamole.Touch#touches touches}.
  14274     */
  14275    this.activeTouches = 0;
  14276
  14277    /**
  14278     * Fired whenever a new touch contact is initiated on the element
  14279     * associated with this Guacamole.Touch.
  14280     * 
  14281     * @event Guacamole.Touch#touchstart
  14282     * @param {!Guacamole.Touch.Event} event
  14283     *     A {@link Guacamole.Touch.Event} object representing the "touchstart"
  14284     *     event.
  14285     */
  14286
  14287    /**
  14288     * Fired whenever an established touch contact moves within the element
  14289     * associated with this Guacamole.Touch.
  14290     * 
  14291     * @event Guacamole.Touch#touchmove
  14292     * @param {!Guacamole.Touch.Event} event
  14293     *     A {@link Guacamole.Touch.Event} object representing the "touchmove"
  14294     *     event.
  14295     */
  14296
  14297    /**
  14298     * Fired whenever an established touch contact is lifted from the element
  14299     * associated with this Guacamole.Touch.
  14300     * 
  14301     * @event Guacamole.Touch#touchend
  14302     * @param {!Guacamole.Touch.Event} event
  14303     *     A {@link Guacamole.Touch.Event} object representing the "touchend"
  14304     *     event.
  14305     */
  14306
  14307    element.addEventListener('touchstart', function touchstart(e) {
  14308
  14309        // Fire "ontouchstart" events for all new touches
  14310        for (var i = 0; i < e.changedTouches.length; i++) {
  14311
  14312            var changedTouch = e.changedTouches[i];
  14313            var identifier = changedTouch.identifier;
  14314
  14315            // Ignore duplicated touches
  14316            if (guacTouch.touches[identifier])
  14317                continue;
  14318
  14319            var touch = guacTouch.touches[identifier] = new Guacamole.Touch.State({
  14320                id      : identifier,
  14321                radiusX : changedTouch.radiusX || DEFAULT_CONTACT_RADIUS,
  14322                radiusY : changedTouch.radiusY || DEFAULT_CONTACT_RADIUS,
  14323                angle   : changedTouch.angle || 0.0,
  14324                force   : changedTouch.force || 1.0 /* Within JavaScript changedTouch events, a force of 0.0 indicates the device does not support reporting changedTouch force */
  14325            });
  14326
  14327            guacTouch.activeTouches++;
  14328
  14329            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
  14330            guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));
  14331
  14332        }
  14333
  14334    }, false);
  14335
  14336    element.addEventListener('touchmove', function touchstart(e) {
  14337
  14338        // Fire "ontouchmove" events for all updated touches
  14339        for (var i = 0; i < e.changedTouches.length; i++) {
  14340
  14341            var changedTouch = e.changedTouches[i];
  14342            var identifier = changedTouch.identifier;
  14343
  14344            // Ignore any unrecognized touches
  14345            var touch = guacTouch.touches[identifier];
  14346            if (!touch)
  14347                continue;
  14348
  14349            // Update force only if supported by browser (otherwise, assume
  14350            // force is unchanged)
  14351            if (changedTouch.force)
  14352                touch.force = changedTouch.force;
  14353
  14354            // Update touch area, if supported by browser and device
  14355            touch.angle = changedTouch.angle || 0.0;
  14356            touch.radiusX = changedTouch.radiusX || DEFAULT_CONTACT_RADIUS;
  14357            touch.radiusY = changedTouch.radiusY || DEFAULT_CONTACT_RADIUS;
  14358
  14359            // Update with any change in position
  14360            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
  14361            guacTouch.dispatch(new Guacamole.Touch.Event('touchmove', e, touch));
  14362
  14363        }
  14364
  14365    }, false);
  14366
  14367    element.addEventListener('touchend', function touchstart(e) {
  14368
  14369        // Fire "ontouchend" events for all updated touches
  14370        for (var i = 0; i < e.changedTouches.length; i++) {
  14371
  14372            var changedTouch = e.changedTouches[i];
  14373            var identifier = changedTouch.identifier;
  14374
  14375            // Ignore any unrecognized touches
  14376            var touch = guacTouch.touches[identifier];
  14377            if (!touch)
  14378                continue;
  14379
  14380            // Stop tracking this particular touch
  14381            delete guacTouch.touches[identifier];
  14382            guacTouch.activeTouches--;
  14383
  14384            // Touch has ended
  14385            touch.force = 0.0;
  14386
  14387            // Update with final position
  14388            touch.fromClientPosition(element, changedTouch.clientX, changedTouch.clientY);
  14389            guacTouch.dispatch(new Guacamole.Touch.Event('touchend', e, touch));
  14390
  14391        }
  14392
  14393    }, false);
  14394
  14395};
  14396
  14397/**
  14398 * The current state of a touch contact.
  14399 *
  14400 * @constructor
  14401 * @augments Guacamole.Position
  14402 * @param {Guacamole.Touch.State|object} [template={}]
  14403 *     The object whose properties should be copied within the new
  14404 *     Guacamole.Touch.State.
  14405 */
  14406Guacamole.Touch.State = function State(template) {
  14407
  14408    template = template || {};
  14409
  14410    Guacamole.Position.call(this, template);
  14411
  14412    /**
  14413     * An arbitrary integer ID which uniquely identifies this contact relative
  14414     * to other active contacts.
  14415     *
  14416     * @type {!number}
  14417     * @default 0
  14418     */
  14419    this.id = template.id || 0;
  14420
  14421    /**
  14422     * The Y radius of the ellipse covering the general area of the touch
  14423     * contact, in pixels.
  14424     *
  14425     * @type {!number}
  14426     * @default 0
  14427     */
  14428    this.radiusX = template.radiusX || 0;
  14429
  14430    /**
  14431     * The X radius of the ellipse covering the general area of the touch
  14432     * contact, in pixels.
  14433     *
  14434     * @type {!number}
  14435     * @default 0
  14436     */
  14437    this.radiusY = template.radiusY || 0;
  14438
  14439    /**
  14440     * The rough angle of clockwise rotation of the general area of the touch
  14441     * contact, in degrees.
  14442     *
  14443     * @type {!number}
  14444     * @default 0.0
  14445     */
  14446    this.angle = template.angle || 0.0;
  14447
  14448    /**
  14449     * The relative force exerted by the touch contact, where 0 is no force
  14450     * (the touch has been lifted) and 1 is maximum force (the maximum amount
  14451     * of force representable by the device).
  14452     *
  14453     * @type {!number}
  14454     * @default 1.0
  14455     */
  14456    this.force = template.force || 1.0;
  14457
  14458};
  14459
  14460/**
  14461 * An event which represents a change in state of a single touch contact,
  14462 * including the creation or removal of that contact. If multiple contacts are
  14463 * involved in a touch interaction, each contact will be associated with its
  14464 * own event.
  14465 *
  14466 * @constructor
  14467 * @augments Guacamole.Event.DOMEvent
  14468 * @param {!string} type
  14469 *     The name of the touch event type. Possible values are "touchstart",
  14470 *     "touchmove", and "touchend".
  14471 *
  14472 * @param {!TouchEvent} event
  14473 *     The DOM touch event that produced this Guacamole.Touch.Event.
  14474 *
  14475 * @param {!Guacamole.Touch.State} state
  14476 *     The state of the touch contact associated with this event.
  14477 */
  14478Guacamole.Touch.Event = function TouchEvent(type, event, state) {
  14479
  14480    Guacamole.Event.DOMEvent.call(this, type, [ event ]);
  14481
  14482    /**
  14483     * The state of the touch contact associated with this event.
  14484     *
  14485     * @type {!Guacamole.Touch.State}
  14486     */
  14487    this.state = state;
  14488
  14489};
  14490/*
  14491 * Licensed to the Apache Software Foundation (ASF) under one
  14492 * or more contributor license agreements.  See the NOTICE file
  14493 * distributed with this work for additional information
  14494 * regarding copyright ownership.  The ASF licenses this file
  14495 * to you under the Apache License, Version 2.0 (the
  14496 * "License"); you may not use this file except in compliance
  14497 * with the License.  You may obtain a copy of the License at
  14498 *
  14499 *   http://www.apache.org/licenses/LICENSE-2.0
  14500 *
  14501 * Unless required by applicable law or agreed to in writing,
  14502 * software distributed under the License is distributed on an
  14503 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  14504 * KIND, either express or implied.  See the License for the
  14505 * specific language governing permissions and limitations
  14506 * under the License.
  14507 */
  14508
  14509var Guacamole = Guacamole || {};
  14510
  14511/**
  14512 * Core object providing abstract communication for Guacamole. This object
  14513 * is a null implementation whose functions do nothing. Guacamole applications
  14514 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
  14515 * on this one.
  14516 * 
  14517 * @constructor
  14518 * @see Guacamole.HTTPTunnel
  14519 */
  14520Guacamole.Tunnel = function() {
  14521
  14522    /**
  14523     * Connect to the tunnel with the given optional data. This data is
  14524     * typically used for authentication. The format of data accepted is
  14525     * up to the tunnel implementation.
  14526     * 
  14527     * @param {string} [data]
  14528     *     The data to send to the tunnel when connecting.
  14529     */
  14530    this.connect = function(data) {};
  14531    
  14532    /**
  14533     * Disconnect from the tunnel.
  14534     */
  14535    this.disconnect = function() {};
  14536    
  14537    /**
  14538     * Send the given message through the tunnel to the service on the other
  14539     * side. All messages are guaranteed to be received in the order sent.
  14540     * 
  14541     * @param {...*} elements
  14542     *     The elements of the message to send to the service on the other side
  14543     *     of the tunnel.
  14544     */
  14545    this.sendMessage = function(elements) {};
  14546
  14547    /**
  14548     * Changes the stored numeric state of this tunnel, firing the onstatechange
  14549     * event if the new state is different and a handler has been defined.
  14550     *
  14551     * @private
  14552     * @param {!number} state
  14553     *     The new state of this tunnel.
  14554     */
  14555    this.setState = function(state) {
  14556
  14557        // Notify only if state changes
  14558        if (state !== this.state) {
  14559            this.state = state;
  14560            if (this.onstatechange)
  14561                this.onstatechange(state);
  14562        }
  14563
  14564    };
  14565
  14566    /**
  14567     * Changes the stored UUID that uniquely identifies this tunnel, firing the
  14568     * onuuid event if a handler has been defined.
  14569     *
  14570     * @private
  14571     * @param {string} uuid
  14572     *     The new state of this tunnel.
  14573     */
  14574    this.setUUID = function setUUID(uuid) {
  14575        this.uuid = uuid;
  14576        if (this.onuuid)
  14577            this.onuuid(uuid);
  14578    };
  14579
  14580    /**
  14581     * Returns whether this tunnel is currently connected.
  14582     *
  14583     * @returns {!boolean}
  14584     *     true if this tunnel is currently connected, false otherwise.
  14585     */
  14586    this.isConnected = function isConnected() {
  14587        return this.state === Guacamole.Tunnel.State.OPEN
  14588            || this.state === Guacamole.Tunnel.State.UNSTABLE;
  14589    };
  14590
  14591    /**
  14592     * The current state of this tunnel.
  14593     * 
  14594     * @type {!number}
  14595     */
  14596    this.state = Guacamole.Tunnel.State.CLOSED;
  14597
  14598    /**
  14599     * The maximum amount of time to wait for data to be received, in
  14600     * milliseconds. If data is not received within this amount of time,
  14601     * the tunnel is closed with an error. The default value is 15000.
  14602     *
  14603     * @type {!number}
  14604     */
  14605    this.receiveTimeout = 15000;
  14606
  14607    /**
  14608     * The amount of time to wait for data to be received before considering
  14609     * the connection to be unstable, in milliseconds. If data is not received
  14610     * within this amount of time, the tunnel status is updated to warn that
  14611     * the connection appears unresponsive and may close. The default value is
  14612     * 1500.
  14613     * 
  14614     * @type {!number}
  14615     */
  14616    this.unstableThreshold = 1500;
  14617
  14618    /**
  14619     * The UUID uniquely identifying this tunnel. If not yet known, this will
  14620     * be null.
  14621     *
  14622     * @type {string}
  14623     */
  14624    this.uuid = null;
  14625
  14626    /**
  14627     * Fired when the UUID that uniquely identifies this tunnel is known.
  14628     *
  14629     * @event
  14630     * @param {!string}
  14631     *     The UUID uniquely identifying this tunnel.
  14632     */
  14633    this.onuuid = null;
  14634
  14635    /**
  14636     * Fired whenever an error is encountered by the tunnel.
  14637     * 
  14638     * @event
  14639     * @param {!Guacamole.Status} status
  14640     *     A status object which describes the error.
  14641     */
  14642    this.onerror = null;
  14643
  14644    /**
  14645     * Fired whenever the state of the tunnel changes.
  14646     * 
  14647     * @event
  14648     * @param {!number} state
  14649     *     The new state of the client.
  14650     */
  14651    this.onstatechange = null;
  14652
  14653    /**
  14654     * Fired once for every complete Guacamole instruction received, in order.
  14655     * 
  14656     * @event
  14657     * @param {!string} opcode
  14658     *     The Guacamole instruction opcode.
  14659     *
  14660     * @param {!string[]} parameters
  14661     *     The parameters provided for the instruction, if any.
  14662     */
  14663    this.oninstruction = null;
  14664
  14665};
  14666
  14667/**
  14668 * The Guacamole protocol instruction opcode reserved for arbitrary internal
  14669 * use by tunnel implementations. The value of this opcode is guaranteed to be
  14670 * the empty string (""). Tunnel implementations may use this opcode for any
  14671 * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP
  14672 * response, and by the WebSocket tunnel to transmit the tunnel UUID and send
  14673 * connection stability test pings/responses.
  14674 *
  14675 * @constant
  14676 * @type {!string}
  14677 */
  14678Guacamole.Tunnel.INTERNAL_DATA_OPCODE = '';
  14679
  14680/**
  14681 * All possible tunnel states.
  14682 *
  14683 * @type {!Object.<string, number>}
  14684 */
  14685Guacamole.Tunnel.State = {
  14686
  14687    /**
  14688     * A connection is in pending. It is not yet known whether connection was
  14689     * successful.
  14690     * 
  14691     * @type {!number}
  14692     */
  14693    "CONNECTING": 0,
  14694
  14695    /**
  14696     * Connection was successful, and data is being received.
  14697     * 
  14698     * @type {!number}
  14699     */
  14700    "OPEN": 1,
  14701
  14702    /**
  14703     * The connection is closed. Connection may not have been successful, the
  14704     * tunnel may have been explicitly closed by either side, or an error may
  14705     * have occurred.
  14706     * 
  14707     * @type {!number}
  14708     */
  14709    "CLOSED": 2,
  14710
  14711    /**
  14712     * The connection is open, but communication through the tunnel appears to
  14713     * be disrupted, and the connection may close as a result.
  14714     *
  14715     * @type {!number}
  14716     */
  14717    "UNSTABLE" : 3
  14718
  14719};
  14720
  14721/**
  14722 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
  14723 * 
  14724 * @constructor
  14725 * @augments Guacamole.Tunnel
  14726 *
  14727 * @param {!string} tunnelURL
  14728 *     The URL of the HTTP tunneling service.
  14729 *
  14730 * @param {boolean} [crossDomain=false]
  14731 *     Whether tunnel requests will be cross-domain, and thus must use CORS
  14732 *     mechanisms and headers. By default, it is assumed that tunnel requests
  14733 *     will be made to the same domain.
  14734 *
  14735 * @param {object} [extraTunnelHeaders={}]
  14736 *     Key value pairs containing the header names and values of any additional
  14737 *     headers to be sent in tunnel requests. By default, no extra headers will
  14738 *     be added.
  14739 */
  14740Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
  14741
  14742    /**
  14743     * Reference to this HTTP tunnel.
  14744     *
  14745     * @private
  14746     * @type {!Guacamole.HTTPTunnel}
  14747     */
  14748    var tunnel = this;
  14749
  14750    var TUNNEL_CONNECT = tunnelURL + "?connect";
  14751    var TUNNEL_READ    = tunnelURL + "?read:";
  14752    var TUNNEL_WRITE   = tunnelURL + "?write:";
  14753
  14754    var POLLING_ENABLED     = 1;
  14755    var POLLING_DISABLED    = 0;
  14756
  14757    // Default to polling - will be turned off automatically if not needed
  14758    var pollingMode = POLLING_ENABLED;
  14759
  14760    var sendingMessages = false;
  14761    var outputMessageBuffer = "";
  14762
  14763    // If requests are expected to be cross-domain, the cookie that the HTTP
  14764    // tunnel depends on will only be sent if withCredentials is true
  14765    var withCredentials = !!crossDomain;
  14766
  14767    /**
  14768     * The current receive timeout ID, if any.
  14769     *
  14770     * @private
  14771     * @type {number}
  14772     */
  14773    var receive_timeout = null;
  14774
  14775    /**
  14776     * The current connection stability timeout ID, if any.
  14777     *
  14778     * @private
  14779     * @type {number}
  14780     */
  14781    var unstableTimeout = null;
  14782
  14783    /**
  14784     * The current connection stability test ping interval ID, if any. This
  14785     * will only be set upon successful connection.
  14786     *
  14787     * @private
  14788     * @type {number}
  14789     */
  14790    var pingInterval = null;
  14791
  14792    /**
  14793     * The number of milliseconds to wait between connection stability test
  14794     * pings.
  14795     *
  14796     * @private
  14797     * @constant
  14798     * @type {!number}
  14799     */
  14800    var PING_FREQUENCY = 500;
  14801
  14802    /**
  14803     * Additional headers to be sent in tunnel requests. This dictionary can be
  14804     * populated with key/value header pairs to pass information such as authentication
  14805     * tokens, etc.
  14806     *
  14807     * @private
  14808     * @type {!object}
  14809     */
  14810    var extraHeaders = extraTunnelHeaders || {};
  14811
  14812    /**
  14813     * The name of the HTTP header containing the session token specific to the
  14814     * HTTP tunnel implementation.
  14815     *
  14816     * @private
  14817     * @constant
  14818     * @type {!string}
  14819     */
  14820    var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token';
  14821
  14822    /**
  14823     * The session token currently assigned to this HTTP tunnel. All distinct
  14824     * HTTP tunnel connections will have their own dedicated session token.
  14825     *
  14826     * @private
  14827     * @type {string}
  14828     */
  14829    var tunnelSessionToken = null;
  14830
  14831    /**
  14832     * Adds the configured additional headers to the given request.
  14833     *
  14834     * @private
  14835     * @param {!XMLHttpRequest} request
  14836     *     The request where the configured extra headers will be added.
  14837     *
  14838     * @param {!object} headers
  14839     *     The headers to be added to the request.
  14840     */
  14841    function addExtraHeaders(request, headers) {
  14842        for (var name in headers) {
  14843            request.setRequestHeader(name, headers[name]);
  14844        }
  14845    }
  14846
  14847    /**
  14848     * Resets the state of timers tracking network activity and stability. If
  14849     * those timers are not yet started, invoking this function starts them.
  14850     * This function should be invoked when the tunnel is established and every
  14851     * time there is network activity on the tunnel, such that the timers can
  14852     * safely assume the network and/or server are not responding if this
  14853     * function has not been invoked for a significant period of time.
  14854     *
  14855     * @private
  14856     */
  14857    var resetTimers = function resetTimers() {
  14858
  14859        // Get rid of old timeouts (if any)
  14860        window.clearTimeout(receive_timeout);
  14861        window.clearTimeout(unstableTimeout);
  14862
  14863        // Clear unstable status
  14864        if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  14865            tunnel.setState(Guacamole.Tunnel.State.OPEN);
  14866
  14867        // Set new timeout for tracking overall connection timeout
  14868        receive_timeout = window.setTimeout(function () {
  14869            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  14870        }, tunnel.receiveTimeout);
  14871
  14872        // Set new timeout for tracking suspected connection instability
  14873        unstableTimeout = window.setTimeout(function() {
  14874            tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  14875        }, tunnel.unstableThreshold);
  14876
  14877    };
  14878
  14879    /**
  14880     * Closes this tunnel, signaling the given status and corresponding
  14881     * message, which will be sent to the onerror handler if the status is
  14882     * an error status.
  14883     * 
  14884     * @private
  14885     * @param {!Guacamole.Status} status
  14886     *     The status causing the connection to close;
  14887     */
  14888    function close_tunnel(status) {
  14889
  14890        // Get rid of old timeouts (if any)
  14891        window.clearTimeout(receive_timeout);
  14892        window.clearTimeout(unstableTimeout);
  14893
  14894        // Cease connection test pings
  14895        window.clearInterval(pingInterval);
  14896
  14897        // Ignore if already closed
  14898        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  14899            return;
  14900
  14901        // If connection closed abnormally, signal error.
  14902        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
  14903
  14904            // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
  14905            // only signals end-of-stream for the HTTP tunnel.
  14906            if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
  14907                    || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
  14908                tunnel.onerror(status);
  14909
  14910        }
  14911
  14912        // Reset output message buffer
  14913        sendingMessages = false;
  14914
  14915        // Mark as closed
  14916        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  14917
  14918    }
  14919
  14920
  14921    this.sendMessage = function() {
  14922
  14923        // Do not attempt to send messages if not connected
  14924        if (!tunnel.isConnected())
  14925            return;
  14926
  14927        // Do not attempt to send empty messages
  14928        if (arguments.length === 0)
  14929            return;
  14930
  14931        /**
  14932         * Converts the given value to a length/string pair for use as an
  14933         * element in a Guacamole instruction.
  14934         * 
  14935         * @private
  14936         * @param value
  14937         *     The value to convert.
  14938         *
  14939         * @return {!string}
  14940         *     The converted value.
  14941         */
  14942        function getElement(value) {
  14943            var string = new String(value);
  14944            return string.length + "." + string; 
  14945        }
  14946
  14947        // Initialized message with first element
  14948        var message = getElement(arguments[0]);
  14949
  14950        // Append remaining elements
  14951        for (var i=1; i<arguments.length; i++)
  14952            message += "," + getElement(arguments[i]);
  14953
  14954        // Final terminator
  14955        message += ";";
  14956
  14957        // Add message to buffer
  14958        outputMessageBuffer += message;
  14959
  14960        // Send if not currently sending
  14961        if (!sendingMessages)
  14962            sendPendingMessages();
  14963
  14964    };
  14965
  14966    function sendPendingMessages() {
  14967
  14968        // Do not attempt to send messages if not connected
  14969        if (!tunnel.isConnected())
  14970            return;
  14971
  14972        if (outputMessageBuffer.length > 0) {
  14973
  14974            sendingMessages = true;
  14975
  14976            var message_xmlhttprequest = new XMLHttpRequest();
  14977            message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
  14978            message_xmlhttprequest.withCredentials = withCredentials;
  14979            addExtraHeaders(message_xmlhttprequest, extraHeaders);
  14980            message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
  14981            message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  14982
  14983            // Once response received, send next queued event.
  14984            message_xmlhttprequest.onreadystatechange = function() {
  14985                if (message_xmlhttprequest.readyState === 4) {
  14986
  14987                    resetTimers();
  14988
  14989                    // If an error occurs during send, handle it
  14990                    if (message_xmlhttprequest.status !== 200)
  14991                        handleHTTPTunnelError(message_xmlhttprequest);
  14992
  14993                    // Otherwise, continue the send loop
  14994                    else
  14995                        sendPendingMessages();
  14996
  14997                }
  14998            };
  14999
  15000            message_xmlhttprequest.send(outputMessageBuffer);
  15001            outputMessageBuffer = ""; // Clear buffer
  15002
  15003        }
  15004        else
  15005            sendingMessages = false;
  15006
  15007    }
  15008
  15009    function handleHTTPTunnelError(xmlhttprequest) {
  15010
  15011        // Pull status code directly from headers provided by Guacamole
  15012        var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
  15013        if (code) {
  15014            var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
  15015            close_tunnel(new Guacamole.Status(code, message));
  15016        }
  15017
  15018        // Failing that, derive a Guacamole status code from the HTTP status
  15019        // code provided by the browser
  15020        else if (xmlhttprequest.status)
  15021            close_tunnel(new Guacamole.Status(
  15022                Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
  15023                    xmlhttprequest.statusText));
  15024
  15025        // Otherwise, assume server is unreachable
  15026        else
  15027            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  15028
  15029    }
  15030
  15031    function handleResponse(xmlhttprequest) {
  15032
  15033        var interval = null;
  15034        var nextRequest = null;
  15035
  15036        var dataUpdateEvents = 0;
  15037
  15038        // The location of the last element's terminator
  15039        var elementEnd = -1;
  15040
  15041        // Where to start the next length search or the next element
  15042        var startIndex = 0;
  15043
  15044        // Parsed elements
  15045        var elements = new Array();
  15046
  15047        function parseResponse() {
  15048
  15049            // Do not handle responses if not connected
  15050            if (!tunnel.isConnected()) {
  15051                
  15052                // Clean up interval if polling
  15053                if (interval !== null)
  15054                    clearInterval(interval);
  15055                
  15056                return;
  15057            }
  15058
  15059            // Do not parse response yet if not ready
  15060            if (xmlhttprequest.readyState < 2) return;
  15061
  15062            // Attempt to read status
  15063            var status;
  15064            try { status = xmlhttprequest.status; }
  15065
  15066            // If status could not be read, assume successful.
  15067            catch (e) { status = 200; }
  15068
  15069            // Start next request as soon as possible IF request was successful
  15070            if (!nextRequest && status === 200)
  15071                nextRequest = makeRequest();
  15072
  15073            // Parse stream when data is received and when complete.
  15074            if (xmlhttprequest.readyState === 3 ||
  15075                xmlhttprequest.readyState === 4) {
  15076
  15077                resetTimers();
  15078
  15079                // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
  15080                if (pollingMode === POLLING_ENABLED) {
  15081                    if (xmlhttprequest.readyState === 3 && !interval)
  15082                        interval = setInterval(parseResponse, 30);
  15083                    else if (xmlhttprequest.readyState === 4 && interval)
  15084                        clearInterval(interval);
  15085                }
  15086
  15087                // If canceled, stop transfer
  15088                if (xmlhttprequest.status === 0) {
  15089                    tunnel.disconnect();
  15090                    return;
  15091                }
  15092
  15093                // Halt on error during request
  15094                else if (xmlhttprequest.status !== 200) {
  15095                    handleHTTPTunnelError(xmlhttprequest);
  15096                    return;
  15097                }
  15098
  15099                // Attempt to read in-progress data
  15100                var current;
  15101                try { current = xmlhttprequest.responseText; }
  15102
  15103                // Do not attempt to parse if data could not be read
  15104                catch (e) { return; }
  15105
  15106                // While search is within currently received data
  15107                while (elementEnd < current.length) {
  15108
  15109                    // If we are waiting for element data
  15110                    if (elementEnd >= startIndex) {
  15111
  15112                        // We now have enough data for the element. Parse.
  15113                        var element = current.substring(startIndex, elementEnd);
  15114                        var terminator = current.substring(elementEnd, elementEnd+1);
  15115
  15116                        // Add element to array
  15117                        elements.push(element);
  15118
  15119                        // If last element, handle instruction
  15120                        if (terminator === ";") {
  15121
  15122                            // Get opcode
  15123                            var opcode = elements.shift();
  15124
  15125                            // Call instruction handler.
  15126                            if (tunnel.oninstruction)
  15127                                tunnel.oninstruction(opcode, elements);
  15128
  15129                            // Clear elements
  15130                            elements.length = 0;
  15131
  15132                        }
  15133
  15134                        // Start searching for length at character after
  15135                        // element terminator
  15136                        startIndex = elementEnd + 1;
  15137
  15138                    }
  15139
  15140                    // Search for end of length
  15141                    var lengthEnd = current.indexOf(".", startIndex);
  15142                    if (lengthEnd !== -1) {
  15143
  15144                        // Parse length
  15145                        var length = parseInt(current.substring(elementEnd+1, lengthEnd));
  15146
  15147                        // If we're done parsing, handle the next response.
  15148                        if (length === 0) {
  15149
  15150                            // Clean up interval if polling
  15151                            if (interval)
  15152                                clearInterval(interval);
  15153                           
  15154                            // Clean up object
  15155                            xmlhttprequest.onreadystatechange = null;
  15156                            xmlhttprequest.abort();
  15157
  15158                            // Start handling next request
  15159                            if (nextRequest)
  15160                                handleResponse(nextRequest);
  15161
  15162                            // Done parsing
  15163                            break;
  15164
  15165                        }
  15166
  15167                        // Calculate start of element
  15168                        startIndex = lengthEnd + 1;
  15169
  15170                        // Calculate location of element terminator
  15171                        elementEnd = startIndex + length;
  15172
  15173                    }
  15174                    
  15175                    // If no period yet, continue search when more data
  15176                    // is received
  15177                    else {
  15178                        startIndex = current.length;
  15179                        break;
  15180                    }
  15181
  15182                } // end parse loop
  15183
  15184            }
  15185
  15186        }
  15187
  15188        // If response polling enabled, attempt to detect if still
  15189        // necessary (via wrapping parseResponse())
  15190        if (pollingMode === POLLING_ENABLED) {
  15191            xmlhttprequest.onreadystatechange = function() {
  15192
  15193                // If we receive two or more readyState==3 events,
  15194                // there is no need to poll.
  15195                if (xmlhttprequest.readyState === 3) {
  15196                    dataUpdateEvents++;
  15197                    if (dataUpdateEvents >= 2) {
  15198                        pollingMode = POLLING_DISABLED;
  15199                        xmlhttprequest.onreadystatechange = parseResponse;
  15200                    }
  15201                }
  15202
  15203                parseResponse();
  15204            };
  15205        }
  15206
  15207        // Otherwise, just parse
  15208        else
  15209            xmlhttprequest.onreadystatechange = parseResponse;
  15210
  15211        parseResponse();
  15212
  15213    }
  15214
  15215    /**
  15216     * Arbitrary integer, unique for each tunnel read request.
  15217     * @private
  15218     */
  15219    var request_id = 0;
  15220
  15221    function makeRequest() {
  15222
  15223        // Make request, increment request ID
  15224        var xmlhttprequest = new XMLHttpRequest();
  15225        xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
  15226        xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken);
  15227        xmlhttprequest.withCredentials = withCredentials;
  15228        addExtraHeaders(xmlhttprequest, extraHeaders);
  15229        xmlhttprequest.send(null);
  15230
  15231        return xmlhttprequest;
  15232
  15233    }
  15234
  15235    this.connect = function(data) {
  15236
  15237        // Start waiting for connect
  15238        resetTimers();
  15239
  15240        // Mark the tunnel as connecting
  15241        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  15242
  15243        // Start tunnel and connect
  15244        var connect_xmlhttprequest = new XMLHttpRequest();
  15245        connect_xmlhttprequest.onreadystatechange = function() {
  15246
  15247            if (connect_xmlhttprequest.readyState !== 4)
  15248                return;
  15249
  15250            // If failure, throw error
  15251            if (connect_xmlhttprequest.status !== 200) {
  15252                handleHTTPTunnelError(connect_xmlhttprequest);
  15253                return;
  15254            }
  15255
  15256            resetTimers();
  15257
  15258            // Get UUID and HTTP-specific tunnel session token from response
  15259            tunnel.setUUID(connect_xmlhttprequest.responseText);
  15260            tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER);
  15261
  15262            // Fail connect attempt if token is not successfully assigned
  15263            if (!tunnelSessionToken) {
  15264                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  15265                return;
  15266            }
  15267
  15268            // Mark as open
  15269            tunnel.setState(Guacamole.Tunnel.State.OPEN);
  15270
  15271            // Ping tunnel endpoint regularly to test connection stability
  15272            pingInterval = setInterval(function sendPing() {
  15273                tunnel.sendMessage("nop");
  15274            }, PING_FREQUENCY);
  15275
  15276            // Start reading data
  15277            handleResponse(makeRequest());
  15278
  15279        };
  15280
  15281        connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
  15282        connect_xmlhttprequest.withCredentials = withCredentials;
  15283        addExtraHeaders(connect_xmlhttprequest, extraHeaders);
  15284        connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
  15285        connect_xmlhttprequest.send(data);
  15286
  15287    };
  15288
  15289    this.disconnect = function() {
  15290        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  15291    };
  15292
  15293};
  15294
  15295Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
  15296
  15297/**
  15298 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
  15299 * 
  15300 * @constructor
  15301 * @augments Guacamole.Tunnel
  15302 * @param {!string} tunnelURL
  15303 *     The URL of the WebSocket tunneling service.
  15304 */
  15305Guacamole.WebSocketTunnel = function(tunnelURL) {
  15306
  15307    /**
  15308     * Reference to this WebSocket tunnel.
  15309     *
  15310     * @private
  15311     * @type {Guacamole.WebSocketTunnel}
  15312     */
  15313    var tunnel = this;
  15314
  15315    /**
  15316     * The WebSocket used by this tunnel.
  15317     * 
  15318     * @private
  15319     * @type {WebSocket}
  15320     */
  15321    var socket = null;
  15322
  15323    /**
  15324     * The current receive timeout ID, if any.
  15325     *
  15326     * @private
  15327     * @type {number}
  15328     */
  15329    var receive_timeout = null;
  15330
  15331    /**
  15332     * The current connection stability timeout ID, if any.
  15333     *
  15334     * @private
  15335     * @type {number}
  15336     */
  15337    var unstableTimeout = null;
  15338
  15339    /**
  15340     * The current connection stability test ping timeout ID, if any. This
  15341     * will only be set upon successful connection.
  15342     *
  15343     * @private
  15344     * @type {number}
  15345     */
  15346    var pingTimeout = null;
  15347
  15348    /**
  15349     * The WebSocket protocol corresponding to the protocol used for the current
  15350     * location.
  15351     *
  15352     * @private
  15353     * @type {!Object.<string, string>}
  15354     */
  15355    var ws_protocol = {
  15356        "http:":  "ws:",
  15357        "https:": "wss:"
  15358    };
  15359
  15360    /**
  15361     * The number of milliseconds to wait between connection stability test
  15362     * pings.
  15363     *
  15364     * @private
  15365     * @constant
  15366     * @type {!number}
  15367     */
  15368    var PING_FREQUENCY = 500;
  15369
  15370    /**
  15371     * The timestamp of the point in time that the last connection stability
  15372     * test ping was sent, in milliseconds elapsed since midnight of January 1,
  15373     * 1970 UTC.
  15374     *
  15375     * @private
  15376     * @type {!number}
  15377     */
  15378    var lastSentPing = 0;
  15379
  15380    // Transform current URL to WebSocket URL
  15381
  15382    // If not already a websocket URL
  15383    if (   tunnelURL.substring(0, 3) !== "ws:"
  15384        && tunnelURL.substring(0, 4) !== "wss:") {
  15385
  15386        var protocol = ws_protocol[window.location.protocol];
  15387
  15388        // If absolute URL, convert to absolute WS URL
  15389        if (tunnelURL.substring(0, 1) === "/")
  15390            tunnelURL =
  15391                protocol
  15392                + "//" + window.location.host
  15393                + tunnelURL;
  15394
  15395        // Otherwise, construct absolute from relative URL
  15396        else {
  15397
  15398            // Get path from pathname
  15399            var slash = window.location.pathname.lastIndexOf("/");
  15400            var path  = window.location.pathname.substring(0, slash + 1);
  15401
  15402            // Construct absolute URL
  15403            tunnelURL =
  15404                protocol
  15405                + "//" + window.location.host
  15406                + path
  15407                + tunnelURL;
  15408
  15409        }
  15410
  15411    }
  15412
  15413    /**
  15414     * Sends an internal "ping" instruction to the Guacamole WebSocket
  15415     * endpoint, verifying network connection stability. If the network is
  15416     * stable, the Guacamole server will receive this instruction and respond
  15417     * with an identical ping.
  15418     *
  15419     * @private
  15420     */
  15421    var sendPing = function sendPing() {
  15422        var currentTime = new Date().getTime();
  15423        tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime);
  15424        lastSentPing = currentTime;
  15425    };
  15426
  15427    /**
  15428     * Resets the state of timers tracking network activity and stability. If
  15429     * those timers are not yet started, invoking this function starts them.
  15430     * This function should be invoked when the tunnel is established and every
  15431     * time there is network activity on the tunnel, such that the timers can
  15432     * safely assume the network and/or server are not responding if this
  15433     * function has not been invoked for a significant period of time.
  15434     *
  15435     * @private
  15436     */
  15437    var resetTimers = function resetTimers() {
  15438
  15439        // Get rid of old timeouts (if any)
  15440        window.clearTimeout(receive_timeout);
  15441        window.clearTimeout(unstableTimeout);
  15442        window.clearTimeout(pingTimeout);
  15443
  15444        // Clear unstable status
  15445        if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
  15446            tunnel.setState(Guacamole.Tunnel.State.OPEN);
  15447
  15448        // Set new timeout for tracking overall connection timeout
  15449        receive_timeout = window.setTimeout(function () {
  15450            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
  15451        }, tunnel.receiveTimeout);
  15452
  15453        // Set new timeout for tracking suspected connection instability
  15454        unstableTimeout = window.setTimeout(function() {
  15455            tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
  15456        }, tunnel.unstableThreshold);
  15457
  15458        var currentTime = new Date().getTime();
  15459        var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0);
  15460
  15461        // Ping tunnel endpoint regularly to test connection stability, sending
  15462        // the ping immediately if enough time has already elapsed
  15463        if (pingDelay > 0)
  15464            pingTimeout = window.setTimeout(sendPing, pingDelay);
  15465        else
  15466            sendPing();
  15467
  15468    };
  15469
  15470    /**
  15471     * Closes this tunnel, signaling the given status and corresponding
  15472     * message, which will be sent to the onerror handler if the status is
  15473     * an error status.
  15474     * 
  15475     * @private
  15476     * @param {!Guacamole.Status} status
  15477     *     The status causing the connection to close;
  15478     */
  15479    function close_tunnel(status) {
  15480
  15481        // Get rid of old timeouts (if any)
  15482        window.clearTimeout(receive_timeout);
  15483        window.clearTimeout(unstableTimeout);
  15484        window.clearTimeout(pingTimeout);
  15485
  15486        // Ignore if already closed
  15487        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
  15488            return;
  15489
  15490        // If connection closed abnormally, signal error.
  15491        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
  15492            tunnel.onerror(status);
  15493
  15494        // Mark as closed
  15495        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  15496
  15497        socket.close();
  15498
  15499    }
  15500
  15501    this.sendMessage = function(elements) {
  15502
  15503        // Do not attempt to send messages if not connected
  15504        if (!tunnel.isConnected())
  15505            return;
  15506
  15507        // Do not attempt to send empty messages
  15508        if (arguments.length === 0)
  15509            return;
  15510
  15511        /**
  15512         * Converts the given value to a length/string pair for use as an
  15513         * element in a Guacamole instruction.
  15514         * 
  15515         * @private
  15516         * @param {*} value
  15517         *     The value to convert.
  15518         *
  15519         * @return {!string}
  15520         *     The converted value.
  15521         */
  15522        function getElement(value) {
  15523            var string = new String(value);
  15524            return string.length + "." + string; 
  15525        }
  15526
  15527        // Initialized message with first element
  15528        var message = getElement(arguments[0]);
  15529
  15530        // Append remaining elements
  15531        for (var i=1; i<arguments.length; i++)
  15532            message += "," + getElement(arguments[i]);
  15533
  15534        // Final terminator
  15535        message += ";";
  15536
  15537        socket.send(message);
  15538
  15539    };
  15540
  15541    this.connect = function(data) {
  15542
  15543        resetTimers();
  15544
  15545        // Mark the tunnel as connecting
  15546        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  15547
  15548        // Connect socket
  15549        socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
  15550
  15551        socket.onopen = function(event) {
  15552            resetTimers();
  15553        };
  15554
  15555        socket.onclose = function(event) {
  15556
  15557            // Pull status code directly from closure reason provided by Guacamole
  15558            if (event.reason)
  15559                close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
  15560
  15561            // Failing that, derive a Guacamole status code from the WebSocket
  15562            // status code provided by the browser
  15563            else if (event.code)
  15564                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));
  15565
  15566            // Otherwise, assume server is unreachable
  15567            else
  15568                close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
  15569
  15570        };
  15571        
  15572        socket.onmessage = function(event) {
  15573
  15574            resetTimers();
  15575
  15576            var message = event.data;
  15577            var startIndex = 0;
  15578            var elementEnd;
  15579
  15580            var elements = [];
  15581
  15582            do {
  15583
  15584                // Search for end of length
  15585                var lengthEnd = message.indexOf(".", startIndex);
  15586                if (lengthEnd !== -1) {
  15587
  15588                    // Parse length
  15589                    var length = parseInt(message.substring(elementEnd+1, lengthEnd));
  15590
  15591                    // Calculate start of element
  15592                    startIndex = lengthEnd + 1;
  15593
  15594                    // Calculate location of element terminator
  15595                    elementEnd = startIndex + length;
  15596
  15597                }
  15598                
  15599                // If no period, incomplete instruction.
  15600                else
  15601                    close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction."));
  15602
  15603                // We now have enough data for the element. Parse.
  15604                var element = message.substring(startIndex, elementEnd);
  15605                var terminator = message.substring(elementEnd, elementEnd+1);
  15606
  15607                // Add element to array
  15608                elements.push(element);
  15609
  15610                // If last element, handle instruction
  15611                if (terminator === ";") {
  15612
  15613                    // Get opcode
  15614                    var opcode = elements.shift();
  15615
  15616                    // Update state and UUID when first instruction received
  15617                    if (tunnel.uuid === null) {
  15618
  15619                        // Associate tunnel UUID if received
  15620                        if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && elements.length === 1)
  15621                            tunnel.setUUID(elements[0]);
  15622
  15623                        // Tunnel is now open and UUID is available
  15624                        tunnel.setState(Guacamole.Tunnel.State.OPEN);
  15625
  15626                    }
  15627
  15628                    // Call instruction handler.
  15629                    if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
  15630                        tunnel.oninstruction(opcode, elements);
  15631
  15632                    // Clear elements
  15633                    elements.length = 0;
  15634
  15635                }
  15636
  15637                // Start searching for length at character after
  15638                // element terminator
  15639                startIndex = elementEnd + 1;
  15640
  15641            } while (startIndex < message.length);
  15642
  15643        };
  15644
  15645    };
  15646
  15647    this.disconnect = function() {
  15648        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
  15649    };
  15650
  15651};
  15652
  15653Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
  15654
  15655/**
  15656 * Guacamole Tunnel which cycles between all specified tunnels until
  15657 * no tunnels are left. Another tunnel is used if an error occurs but
  15658 * no instructions have been received. If an instruction has been
  15659 * received, or no tunnels remain, the error is passed directly out
  15660 * through the onerror handler (if defined).
  15661 * 
  15662 * @constructor
  15663 * @augments Guacamole.Tunnel
  15664 * @param {...Guacamole.Tunnel} tunnelChain
  15665 *     The tunnels to use, in order of priority.
  15666 */
  15667Guacamole.ChainedTunnel = function(tunnelChain) {
  15668
  15669    /**
  15670     * Reference to this chained tunnel.
  15671     * @private
  15672     */
  15673    var chained_tunnel = this;
  15674
  15675    /**
  15676     * Data passed in via connect(), to be used for
  15677     * wrapped calls to other tunnels' connect() functions.
  15678     * @private
  15679     */
  15680    var connect_data;
  15681
  15682    /**
  15683     * Array of all tunnels passed to this ChainedTunnel through the
  15684     * constructor arguments.
  15685     * @private
  15686     */
  15687    var tunnels = [];
  15688
  15689    /**
  15690     * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
  15691     * has yet been committed.
  15692     *
  15693     * @private
  15694     * @type {Guacamole.Tunnel}
  15695     */
  15696    var committedTunnel = null;
  15697
  15698    // Load all tunnels into array
  15699    for (var i=0; i<arguments.length; i++)
  15700        tunnels.push(arguments[i]);
  15701
  15702    /**
  15703     * Sets the current tunnel.
  15704     * 
  15705     * @private
  15706     * @param {!Guacamole.Tunnel} tunnel
  15707     *     The tunnel to set as the current tunnel.
  15708     */
  15709    function attach(tunnel) {
  15710
  15711        // Set own functions to tunnel's functions
  15712        chained_tunnel.disconnect  = tunnel.disconnect;
  15713        chained_tunnel.sendMessage = tunnel.sendMessage;
  15714
  15715        /**
  15716         * Fails the currently-attached tunnel, attaching a new tunnel if
  15717         * possible.
  15718         *
  15719         * @private
  15720         * @param {Guacamole.Status} [status]
  15721         *     An object representing the failure that occured in the
  15722         *     currently-attached tunnel, if known.
  15723         *
  15724         * @return {Guacamole.Tunnel}
  15725         *     The next tunnel, or null if there are no more tunnels to try or
  15726         *     if no more tunnels should be tried.
  15727         */
  15728        var failTunnel = function failTunnel(status) {
  15729
  15730            // Do not attempt to continue using next tunnel on server timeout
  15731            if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) {
  15732                tunnels = [];
  15733                return null;
  15734            }
  15735
  15736            // Get next tunnel
  15737            var next_tunnel = tunnels.shift();
  15738
  15739            // If there IS a next tunnel, try using it.
  15740            if (next_tunnel) {
  15741                tunnel.onerror = null;
  15742                tunnel.oninstruction = null;
  15743                tunnel.onstatechange = null;
  15744                attach(next_tunnel);
  15745            }
  15746
  15747            return next_tunnel;
  15748
  15749        };
  15750
  15751        /**
  15752         * Use the current tunnel from this point forward. Do not try any more
  15753         * tunnels, even if the current tunnel fails.
  15754         * 
  15755         * @private
  15756         */
  15757        function commit_tunnel() {
  15758
  15759            tunnel.onstatechange = chained_tunnel.onstatechange;
  15760            tunnel.oninstruction = chained_tunnel.oninstruction;
  15761            tunnel.onerror = chained_tunnel.onerror;
  15762
  15763            // Assign UUID if already known
  15764            if (tunnel.uuid)
  15765                chained_tunnel.setUUID(tunnel.uuid);
  15766
  15767            // Assign any future received UUIDs such that they are
  15768            // accessible from the main uuid property of the chained tunnel
  15769            tunnel.onuuid = function uuidReceived(uuid) {
  15770                chained_tunnel.setUUID(uuid);
  15771            };
  15772
  15773            committedTunnel = tunnel;
  15774
  15775        }
  15776
  15777        // Wrap own onstatechange within current tunnel
  15778        tunnel.onstatechange = function(state) {
  15779
  15780            switch (state) {
  15781
  15782                // If open, use this tunnel from this point forward.
  15783                case Guacamole.Tunnel.State.OPEN:
  15784                    commit_tunnel();
  15785                    if (chained_tunnel.onstatechange)
  15786                        chained_tunnel.onstatechange(state);
  15787                    break;
  15788
  15789                // If closed, mark failure, attempt next tunnel
  15790                case Guacamole.Tunnel.State.CLOSED:
  15791                    if (!failTunnel() && chained_tunnel.onstatechange)
  15792                        chained_tunnel.onstatechange(state);
  15793                    break;
  15794                
  15795            }
  15796
  15797        };
  15798
  15799        // Wrap own oninstruction within current tunnel
  15800        tunnel.oninstruction = function(opcode, elements) {
  15801
  15802            // Accept current tunnel
  15803            commit_tunnel();
  15804
  15805            // Invoke handler
  15806            if (chained_tunnel.oninstruction)
  15807                chained_tunnel.oninstruction(opcode, elements);
  15808
  15809        };
  15810
  15811        // Attach next tunnel on error
  15812        tunnel.onerror = function(status) {
  15813
  15814            // Mark failure, attempt next tunnel
  15815            if (!failTunnel(status) && chained_tunnel.onerror)
  15816                chained_tunnel.onerror(status);
  15817
  15818        };
  15819
  15820        // Attempt connection
  15821        tunnel.connect(connect_data);
  15822        
  15823    }
  15824
  15825    this.connect = function(data) {
  15826       
  15827        // Remember connect data
  15828        connect_data = data;
  15829
  15830        // Get committed tunnel if exists or the first tunnel on the list
  15831        var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();
  15832
  15833        // Attach first tunnel
  15834        if (next_tunnel)
  15835            attach(next_tunnel);
  15836
  15837        // If there IS no first tunnel, error
  15838        else if (chained_tunnel.onerror)
  15839            chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
  15840
  15841    };
  15842    
  15843};
  15844
  15845Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
  15846
  15847/**
  15848 * Guacamole Tunnel which replays a Guacamole protocol dump from a static file
  15849 * received via HTTP. Instructions within the file are parsed and handled as
  15850 * quickly as possible, while the file is being downloaded.
  15851 *
  15852 * @constructor
  15853 * @augments Guacamole.Tunnel
  15854 * @param {!string} url
  15855 *     The URL of a Guacamole protocol dump.
  15856 *
  15857 * @param {boolean} [crossDomain=false]
  15858 *     Whether tunnel requests will be cross-domain, and thus must use CORS
  15859 *     mechanisms and headers. By default, it is assumed that tunnel requests
  15860 *     will be made to the same domain.
  15861 *
  15862 * @param {object} [extraTunnelHeaders={}]
  15863 *     Key value pairs containing the header names and values of any additional
  15864 *     headers to be sent in tunnel requests. By default, no extra headers will
  15865 *     be added.
  15866 */
  15867Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) {
  15868
  15869    /**
  15870     * Reference to this Guacamole.StaticHTTPTunnel.
  15871     *
  15872     * @private
  15873     */
  15874    var tunnel = this;
  15875
  15876    /**
  15877     * AbortController instance which allows the current, in-progress HTTP
  15878     * request to be aborted. If no request is currently in progress, this will
  15879     * be null.
  15880     *
  15881     * @private
  15882     * @type {AbortController}
  15883     */
  15884    var abortController = null;
  15885
  15886    /**
  15887     * Additional headers to be sent in tunnel requests. This dictionary can be
  15888     * populated with key/value header pairs to pass information such as authentication
  15889     * tokens, etc.
  15890     *
  15891     * @private
  15892     * @type {!object}
  15893     */
  15894    var extraHeaders = extraTunnelHeaders || {};
  15895
  15896    /**
  15897     * The number of bytes in the file being downloaded, or null if this is not
  15898     * known.
  15899     *
  15900     * @type {number}
  15901     */
  15902    this.size = null;
  15903
  15904    this.sendMessage = function sendMessage(elements) {
  15905        // Do nothing
  15906    };
  15907
  15908    this.connect = function connect(data) {
  15909
  15910        // Ensure any existing connection is killed
  15911        tunnel.disconnect();
  15912
  15913        // Connection is now starting
  15914        tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
  15915
  15916        // Create Guacamole protocol and UTF-8 parsers specifically for this
  15917        // connection
  15918        var parser = new Guacamole.Parser();
  15919        var utf8Parser = new Guacamole.UTF8Parser();
  15920
  15921        // Invoke tunnel's oninstruction handler for each parsed instruction
  15922        parser.oninstruction = function instructionReceived(opcode, args) {
  15923            if (tunnel.oninstruction)
  15924                tunnel.oninstruction(opcode, args);
  15925        };
  15926
  15927        // Allow new request to be aborted
  15928        abortController = new AbortController();
  15929
  15930        // Stream using the Fetch API
  15931        fetch(url, {
  15932            headers : extraHeaders,
  15933            credentials : crossDomain ? 'include' : 'same-origin',
  15934            signal : abortController.signal
  15935        })
  15936        .then(function gotResponse(response) {
  15937
  15938            // Reset state and close upon error
  15939            if (!response.ok) {
  15940
  15941                if (tunnel.onerror)
  15942                    tunnel.onerror(new Guacamole.Status(
  15943                        Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));
  15944
  15945                tunnel.disconnect();
  15946                return;
  15947
  15948            }
  15949
  15950            // Report overall size of stream in bytes, if known
  15951            tunnel.size = response.headers.get('Content-Length');
  15952
  15953            // Connection is open
  15954            tunnel.setState(Guacamole.Tunnel.State.OPEN);
  15955
  15956            var reader = response.body.getReader();
  15957            var processReceivedText = function processReceivedText(result) {
  15958
  15959                // Clean up and close when done
  15960                if (result.done) {
  15961                    tunnel.disconnect();
  15962                    return;
  15963                }
  15964
  15965                // Parse only the portion of data which is newly received
  15966                parser.receive(utf8Parser.decode(result.value));
  15967
  15968                // Continue parsing when next chunk is received
  15969                reader.read().then(processReceivedText);
  15970
  15971            };
  15972
  15973            // Schedule parse of first chunk
  15974            reader.read().then(processReceivedText);
  15975
  15976        });
  15977
  15978    };
  15979
  15980    this.disconnect = function disconnect() {
  15981
  15982        // Abort any in-progress request
  15983        if (abortController) {
  15984            abortController.abort();
  15985            abortController = null;
  15986        }
  15987
  15988        // Connection is now closed
  15989        tunnel.setState(Guacamole.Tunnel.State.CLOSED);
  15990
  15991    };
  15992
  15993};
  15994
  15995Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel();
  15996/*
  15997 * Licensed to the Apache Software Foundation (ASF) under one
  15998 * or more contributor license agreements.  See the NOTICE file
  15999 * distributed with this work for additional information
  16000 * regarding copyright ownership.  The ASF licenses this file
  16001 * to you under the Apache License, Version 2.0 (the
  16002 * "License"); you may not use this file except in compliance
  16003 * with the License.  You may obtain a copy of the License at
  16004 *
  16005 *   http://www.apache.org/licenses/LICENSE-2.0
  16006 *
  16007 * Unless required by applicable law or agreed to in writing,
  16008 * software distributed under the License is distributed on an
  16009 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16010 * KIND, either express or implied.  See the License for the
  16011 * specific language governing permissions and limitations
  16012 * under the License.
  16013 */
  16014
  16015var Guacamole = Guacamole || {};
  16016
  16017/**
  16018 * Parser that decodes UTF-8 text from a series of provided ArrayBuffers.
  16019 * Multi-byte characters that continue from one buffer to the next are handled
  16020 * correctly.
  16021 *
  16022 * @constructor
  16023 */
  16024Guacamole.UTF8Parser = function UTF8Parser() {
  16025
  16026    /**
  16027     * The number of bytes remaining for the current codepoint.
  16028     *
  16029     * @private
  16030     * @type {!number}
  16031     */
  16032    var bytesRemaining = 0;
  16033
  16034    /**
  16035     * The current codepoint value, as calculated from bytes read so far.
  16036     *
  16037     * @private
  16038     * @type {!number}
  16039     */
  16040    var codepoint = 0;
  16041
  16042    /**
  16043     * Decodes the given UTF-8 data into a Unicode string, returning a string
  16044     * containing all complete UTF-8 characters within the provided data. The
  16045     * data may end in the middle of a multi-byte character, in which case the
  16046     * complete character will be returned from a later call to decode() after
  16047     * enough bytes have been provided.
  16048     *
  16049     * @private
  16050     * @param {!ArrayBuffer} buffer
  16051     *     Arbitrary UTF-8 data.
  16052     *
  16053     * @return {!string}
  16054     *     The decoded Unicode string.
  16055     */
  16056    this.decode = function decode(buffer) {
  16057
  16058        var text = '';
  16059
  16060        var bytes = new Uint8Array(buffer);
  16061        for (var i=0; i<bytes.length; i++) {
  16062
  16063            // Get current byte
  16064            var value = bytes[i];
  16065
  16066            // Start new codepoint if nothing yet read
  16067            if (bytesRemaining === 0) {
  16068
  16069                // 1 byte (0xxxxxxx)
  16070                if ((value | 0x7F) === 0x7F)
  16071                    text += String.fromCharCode(value);
  16072
  16073                // 2 byte (110xxxxx)
  16074                else if ((value | 0x1F) === 0xDF) {
  16075                    codepoint = value & 0x1F;
  16076                    bytesRemaining = 1;
  16077                }
  16078
  16079                // 3 byte (1110xxxx)
  16080                else if ((value | 0x0F )=== 0xEF) {
  16081                    codepoint = value & 0x0F;
  16082                    bytesRemaining = 2;
  16083                }
  16084
  16085                // 4 byte (11110xxx)
  16086                else if ((value | 0x07) === 0xF7) {
  16087                    codepoint = value & 0x07;
  16088                    bytesRemaining = 3;
  16089                }
  16090
  16091                // Invalid byte
  16092                else
  16093                    text += '\uFFFD';
  16094
  16095            }
  16096
  16097            // Continue existing codepoint (10xxxxxx)
  16098            else if ((value | 0x3F) === 0xBF) {
  16099
  16100                codepoint = (codepoint << 6) | (value & 0x3F);
  16101                bytesRemaining--;
  16102
  16103                // Write codepoint if finished
  16104                if (bytesRemaining === 0)
  16105                    text += String.fromCharCode(codepoint);
  16106
  16107            }
  16108
  16109            // Invalid byte
  16110            else {
  16111                bytesRemaining = 0;
  16112                text += '\uFFFD';
  16113            }
  16114
  16115        }
  16116
  16117        return text;
  16118
  16119    };
  16120
  16121};/*
  16122 * Licensed to the Apache Software Foundation (ASF) under one
  16123 * or more contributor license agreements.  See the NOTICE file
  16124 * distributed with this work for additional information
  16125 * regarding copyright ownership.  The ASF licenses this file
  16126 * to you under the Apache License, Version 2.0 (the
  16127 * "License"); you may not use this file except in compliance
  16128 * with the License.  You may obtain a copy of the License at
  16129 *
  16130 *   http://www.apache.org/licenses/LICENSE-2.0
  16131 *
  16132 * Unless required by applicable law or agreed to in writing,
  16133 * software distributed under the License is distributed on an
  16134 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16135 * KIND, either express or implied.  See the License for the
  16136 * specific language governing permissions and limitations
  16137 * under the License.
  16138 */
  16139
  16140var Guacamole = Guacamole || {};
  16141
  16142/**
  16143 * The unique ID of this version of the Guacamole JavaScript API. This ID will
  16144 * be the version string of the guacamole-common-js Maven project, and can be
  16145 * used in downstream applications as a sanity check that the proper version
  16146 * of the APIs is being used (in case an older version is cached, for example).
  16147 *
  16148 * @type {!string}
  16149 */
  16150Guacamole.API_VERSION = "1.5.0";
  16151/*
  16152 * Licensed to the Apache Software Foundation (ASF) under one
  16153 * or more contributor license agreements.  See the NOTICE file
  16154 * distributed with this work for additional information
  16155 * regarding copyright ownership.  The ASF licenses this file
  16156 * to you under the Apache License, Version 2.0 (the
  16157 * "License"); you may not use this file except in compliance
  16158 * with the License.  You may obtain a copy of the License at
  16159 *
  16160 *   http://www.apache.org/licenses/LICENSE-2.0
  16161 *
  16162 * Unless required by applicable law or agreed to in writing,
  16163 * software distributed under the License is distributed on an
  16164 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16165 * KIND, either express or implied.  See the License for the
  16166 * specific language governing permissions and limitations
  16167 * under the License.
  16168 */
  16169
  16170var Guacamole = Guacamole || {};
  16171
  16172/**
  16173 * Abstract video player which accepts, queues and plays back arbitrary video
  16174 * data. It is up to implementations of this class to provide some means of
  16175 * handling a provided Guacamole.InputStream and rendering the received data to
  16176 * the provided Guacamole.Display.VisibleLayer. Data received along the
  16177 * provided stream is to be played back immediately.
  16178 *
  16179 * @constructor
  16180 */
  16181Guacamole.VideoPlayer = function VideoPlayer() {
  16182
  16183    /**
  16184     * Notifies this Guacamole.VideoPlayer that all video up to the current
  16185     * point in time has been given via the underlying stream, and that any
  16186     * difference in time between queued video data and the current time can be
  16187     * considered latency.
  16188     */
  16189    this.sync = function sync() {
  16190        // Default implementation - do nothing
  16191    };
  16192
  16193};
  16194
  16195/**
  16196 * Determines whether the given mimetype is supported by any built-in
  16197 * implementation of Guacamole.VideoPlayer, and thus will be properly handled
  16198 * by Guacamole.VideoPlayer.getInstance().
  16199 *
  16200 * @param {!string} mimetype
  16201 *     The mimetype to check.
  16202 *
  16203 * @returns {!boolean}
  16204 *     true if the given mimetype is supported by any built-in
  16205 *     Guacamole.VideoPlayer, false otherwise.
  16206 */
  16207Guacamole.VideoPlayer.isSupportedType = function isSupportedType(mimetype) {
  16208
  16209    // There are currently no built-in video players (and therefore no
  16210    // supported types)
  16211    return false;
  16212
  16213};
  16214
  16215/**
  16216 * Returns a list of all mimetypes supported by any built-in
  16217 * Guacamole.VideoPlayer, in rough order of priority. Beware that only the core
  16218 * mimetypes themselves will be listed. Any mimetype parameters, even required
  16219 * ones, will not be included in the list.
  16220 *
  16221 * @returns {!string[]}
  16222 *     A list of all mimetypes supported by any built-in Guacamole.VideoPlayer,
  16223 *     excluding any parameters.
  16224 */
  16225Guacamole.VideoPlayer.getSupportedTypes = function getSupportedTypes() {
  16226
  16227    // There are currently no built-in video players (and therefore no
  16228    // supported types)
  16229    return [];
  16230
  16231};
  16232
  16233/**
  16234 * Returns an instance of Guacamole.VideoPlayer providing support for the given
  16235 * video format. If support for the given video format is not available, null
  16236 * is returned.
  16237 *
  16238 * @param {!Guacamole.InputStream} stream
  16239 *     The Guacamole.InputStream to read video data from.
  16240 *
  16241 * @param {!Guacamole.Display.VisibleLayer} layer
  16242 *     The destination layer in which this Guacamole.VideoPlayer should play
  16243 *     the received video data.
  16244 *
  16245 * @param {!string} mimetype
  16246 *     The mimetype of the video data in the provided stream.
  16247 *
  16248 * @return {Guacamole.VideoPlayer}
  16249 *     A Guacamole.VideoPlayer instance supporting the given mimetype and
  16250 *     reading from the given stream, or null if support for the given mimetype
  16251 *     is absent.
  16252 */
  16253Guacamole.VideoPlayer.getInstance = function getInstance(stream, layer, mimetype) {
  16254
  16255    // There are currently no built-in video players
  16256    return null;
  16257
  16258};
  16259module.exports = Guacamole;