Fix jsdoc, add missing documentation.
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is guacamole-common-js.
16  *
17  * The Initial Developer of the Original Code is
18  * Michael Jumper.
19  * Portions created by the Initial Developer are Copyright (C) 2010
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  * Matt Hortman
24  *
25  * Alternatively, the contents of this file may be used under the terms of
26  * either the GNU General Public License Version 2 or later (the "GPL"), or
27  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
28  * in which case the provisions of the GPL or the LGPL are applicable instead
29  * of those above. If you wish to allow use of your version of this file only
30  * under the terms of either the GPL or the LGPL, and not to allow others to
31  * use your version of this file under the terms of the MPL, indicate your
32  * decision by deleting the provisions above and replace them with the notice
33  * and other provisions required by the GPL or the LGPL. If you do not delete
34  * the provisions above, a recipient may use your version of this file under
35  * the terms of any one of the MPL, the GPL or the LGPL.
36  *
37  * ***** END LICENSE BLOCK ***** */
38
39 /**
40  * Namespace for all Guacamole JavaScript objects.
41  * @namespace
42  */
43 var Guacamole = Guacamole || {};
44
45
46 /**
47  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
48  * automatically handles incoming and outgoing Guacamole instructions via the
49  * provided tunnel, updating the display using one or more canvas elements.
50  * 
51  * @constructor
52  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
53  *                                  Guacamole instructions.
54  */
55 Guacamole.Client = function(tunnel) {
56
57     var guac_client = this;
58
59     var STATE_IDLE          = 0;
60     var STATE_CONNECTING    = 1;
61     var STATE_WAITING       = 2;
62     var STATE_CONNECTED     = 3;
63     var STATE_DISCONNECTING = 4;
64     var STATE_DISCONNECTED  = 5;
65
66     var currentState = STATE_IDLE;
67     
68     var currentTimestamp = 0;
69     var pingInterval = null;
70
71     var displayWidth = 0;
72     var displayHeight = 0;
73     var displayScale = 1;
74
75     /**
76      * Translation from Guacamole protocol line caps to Layer line caps.
77      * @private
78      */
79     var lineCap = {
80         0: "butt",
81         1: "round",
82         2: "square"
83     };
84
85     /**
86      * Translation from Guacamole protocol line caps to Layer line caps.
87      * @private
88      */
89     var lineJoin = {
90         0: "bevel",
91         1: "miter",
92         2: "round"
93     };
94
95     // Create bounding div 
96     var bounds = document.createElement("div");
97     bounds.style.position = "relative";
98     bounds.style.width = (displayWidth*displayScale) + "px";
99     bounds.style.height = (displayHeight*displayScale) + "px";
100
101     // Create display
102     var display = document.createElement("div");
103     display.style.position = "relative";
104     display.style.width = displayWidth + "px";
105     display.style.height = displayHeight + "px";
106
107     // Ensure transformations on display originate at 0,0
108     display.style.transformOrigin =
109     display.style.webkitTransformOrigin =
110     display.style.MozTransformOrigin =
111     display.style.OTransformOrigin =
112     display.style.msTransformOrigin =
113         "0 0";
114
115     // Create default layer
116     var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
117
118     // Position default layer
119     var default_layer_container_element = default_layer_container.getElement();
120     default_layer_container_element.style.position = "absolute";
121     default_layer_container_element.style.left = "0px";
122     default_layer_container_element.style.top  = "0px";
123     default_layer_container_element.style.overflow = "hidden";
124
125     // Create cursor layer
126     var cursor = new Guacamole.Client.LayerContainer(0, 0);
127     cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
128
129     // Position cursor layer
130     var cursor_element = cursor.getElement();
131     cursor_element.style.position = "absolute";
132     cursor_element.style.left = "0px";
133     cursor_element.style.top  = "0px";
134
135     // Add default layer and cursor to display
136     display.appendChild(default_layer_container.getElement());
137     display.appendChild(cursor.getElement());
138
139     // Add display to bounds
140     bounds.appendChild(display);
141
142     // Initially, only default layer exists
143     var layers =  [default_layer_container];
144
145     // No initial buffers
146     var buffers = [];
147
148     tunnel.onerror = function(message) {
149         if (guac_client.onerror)
150             guac_client.onerror(message);
151     };
152
153     function setState(state) {
154         if (state != currentState) {
155             currentState = state;
156             if (guac_client.onstatechange)
157                 guac_client.onstatechange(currentState);
158         }
159     }
160
161     function isConnected() {
162         return currentState == STATE_CONNECTED
163             || currentState == STATE_WAITING;
164     }
165
166     var cursorHotspotX = 0;
167     var cursorHotspotY = 0;
168
169     var cursorX = 0;
170     var cursorY = 0;
171
172     function moveCursor(x, y) {
173
174         // Move cursor layer
175         cursor.translate(x - cursorHotspotX, y - cursorHotspotY);
176
177         // Update stored position
178         cursorX = x;
179         cursorY = y;
180
181     }
182
183     /**
184      * Returns an element containing the display of this Guacamole.Client.
185      * Adding the element returned by this function to an element in the body
186      * of a document will cause the client's display to be visible.
187      * 
188      * @return {Element} An element containing ths display of this
189      *                   Guacamole.Client.
190      */
191     this.getDisplay = function() {
192         return bounds;
193     };
194
195     /**
196      * Sends a key event having the given properties as if the user
197      * pressed or released a key.
198      * 
199      * @param {Boolean} pressed Whether the key is pressed (true) or released
200      *                          (false).
201      * @param {Number} keysym The keysym of the key being pressed or released.
202      */
203     this.sendKeyEvent = function(pressed, keysym) {
204         // Do not send requests if not connected
205         if (!isConnected())
206             return;
207
208         tunnel.sendMessage("key", keysym, pressed);
209     };
210
211     /**
212      * Sends a mouse event having the properties provided by the given mouse
213      * state.
214      * 
215      * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send
216      *                                           in the mouse event.
217      */
218     this.sendMouseState = function(mouseState) {
219
220         // Do not send requests if not connected
221         if (!isConnected())
222             return;
223
224         // Update client-side cursor
225         moveCursor(
226             Math.floor(mouseState.x),
227             Math.floor(mouseState.y)
228         );
229
230         // Build mask
231         var buttonMask = 0;
232         if (mouseState.left)   buttonMask |= 1;
233         if (mouseState.middle) buttonMask |= 2;
234         if (mouseState.right)  buttonMask |= 4;
235         if (mouseState.up)     buttonMask |= 8;
236         if (mouseState.down)   buttonMask |= 16;
237
238         // Send message
239         tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask);
240     };
241
242     /**
243      * Sets the clipboard of the remote client to the given text data.
244      * 
245      * @param {String} data The data to send as the clipboard contents.
246      */
247     this.setClipboard = function(data) {
248
249         // Do not send requests if not connected
250         if (!isConnected())
251             return;
252
253         tunnel.sendMessage("clipboard", data);
254     };
255
256     /**
257      * Fired whenever the state of this Guacamole.Client changes.
258      * 
259      * @event
260      * @param {Number} state The new state of the client.
261      */
262     this.onstatechange = null;
263
264     /**
265      * Fired when the remote client sends a name update.
266      * 
267      * @event
268      * @param {String} name The new name of this client.
269      */
270     this.onname = null;
271
272     /**
273      * Fired when an error is reported by the remote client, and the connection
274      * is being closed.
275      * 
276      * @event
277      * @param {String} error A human-readable description of the error.
278      */
279     this.onerror = null;
280
281     /**
282      * Fired when the clipboard of the remote client is changing.
283      * 
284      * @event
285      * @param {String} data The new text data of the remote clipboard.
286      */
287     this.onclipboard = null;
288
289     // Layers
290     function getBufferLayer(index) {
291
292         index = -1 - index;
293         var buffer = buffers[index];
294
295         // Create buffer if necessary
296         if (buffer == null) {
297             buffer = new Guacamole.Layer(0, 0);
298             buffer.autosize = 1;
299             buffers[index] = buffer;
300         }
301
302         return buffer;
303
304     }
305
306     function getLayerContainer(index) {
307
308         var layer = layers[index];
309         if (layer == null) {
310
311             // Add new layer
312             layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
313             layers[index] = layer;
314
315             // Get and position layer
316             var layer_element = layer.getElement();
317             layer_element.style.position = "absolute";
318             layer_element.style.left = "0px";
319             layer_element.style.top = "0px";
320             layer_element.style.overflow = "hidden";
321
322             // Add to default layer container
323             default_layer_container.getElement().appendChild(layer_element);
324
325         }
326
327         return layer;
328
329     }
330
331     function getLayer(index) {
332        
333         // If buffer, just get layer
334         if (index < 0)
335             return getBufferLayer(index);
336
337         // Otherwise, retrieve layer from layer container
338         return getLayerContainer(index).getLayer();
339
340     }
341
342     /**
343      * Handlers for all defined layer properties.
344      * @private
345      */
346     var layerPropertyHandlers = {
347
348         "miter-limit": function(layer, value) {
349             layer.setMiterLimit(parseFloat(value));
350         }
351
352     };
353     
354     /**
355      * Handlers for all instruction opcodes receivable by a Guacamole protocol
356      * client.
357      * @private
358      */
359     var instructionHandlers = {
360
361         "arc": function(parameters) {
362
363             var layer = getLayer(parseInt(parameters[0]));
364             var x = parseInt(parameters[1]);
365             var y = parseInt(parameters[2]);
366             var radius = parseInt(parameters[3]);
367             var startAngle = parseFloat(parameters[4]);
368             var endAngle = parseFloat(parameters[5]);
369             var negative = parseInt(parameters[6]);
370
371             layer.arc(x, y, radius, startAngle, endAngle, negative != 0);
372
373         },
374
375         "cfill": function(parameters) {
376
377             var channelMask = parseInt(parameters[0]);
378             var layer = getLayer(parseInt(parameters[1]));
379             var r = parseInt(parameters[2]);
380             var g = parseInt(parameters[3]);
381             var b = parseInt(parameters[4]);
382             var a = parseInt(parameters[5]);
383
384             layer.setChannelMask(channelMask);
385
386             layer.fillColor(r, g, b, a);
387
388         },
389
390         "clip": function(parameters) {
391
392             var layer = getLayer(parseInt(parameters[0]));
393
394             layer.clip();
395
396         },
397
398         "clipboard": function(parameters) {
399             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
400         },
401
402         "close": function(parameters) {
403
404             var layer = getLayer(parseInt(parameters[0]));
405
406             layer.close();
407
408         },
409
410         "copy": function(parameters) {
411
412             var srcL = getLayer(parseInt(parameters[0]));
413             var srcX = parseInt(parameters[1]);
414             var srcY = parseInt(parameters[2]);
415             var srcWidth = parseInt(parameters[3]);
416             var srcHeight = parseInt(parameters[4]);
417             var channelMask = parseInt(parameters[5]);
418             var dstL = getLayer(parseInt(parameters[6]));
419             var dstX = parseInt(parameters[7]);
420             var dstY = parseInt(parameters[8]);
421
422             dstL.setChannelMask(channelMask);
423
424             dstL.copy(
425                 srcL,
426                 srcX,
427                 srcY,
428                 srcWidth, 
429                 srcHeight, 
430                 dstX,
431                 dstY 
432             );
433
434         },
435
436         "cstroke": function(parameters) {
437
438             var channelMask = parseInt(parameters[0]);
439             var layer = getLayer(parseInt(parameters[1]));
440             var cap = lineCap[parseInt(parameters[2])];
441             var join = lineJoin[parseInt(parameters[3])];
442             var thickness = parseInt(parameters[4]);
443             var r = parseInt(parameters[5]);
444             var g = parseInt(parameters[6]);
445             var b = parseInt(parameters[7]);
446             var a = parseInt(parameters[8]);
447
448             layer.setChannelMask(channelMask);
449
450             layer.strokeColor(cap, join, thickness, r, g, b, a);
451
452         },
453
454         "cursor": function(parameters) {
455
456             cursorHotspotX = parseInt(parameters[0]);
457             cursorHotspotY = parseInt(parameters[1]);
458             var srcL = getLayer(parseInt(parameters[2]));
459             var srcX = parseInt(parameters[3]);
460             var srcY = parseInt(parameters[4]);
461             var srcWidth = parseInt(parameters[5]);
462             var srcHeight = parseInt(parameters[6]);
463
464             // Reset cursor size
465             cursor.resize(srcWidth, srcHeight);
466
467             // Draw cursor to cursor layer
468             cursor.getLayer().copy(
469                 srcL,
470                 srcX,
471                 srcY,
472                 srcWidth, 
473                 srcHeight, 
474                 0,
475                 0 
476             );
477
478             // Update cursor position (hotspot may have changed)
479             moveCursor(cursorX, cursorY);
480
481         },
482
483         "curve": function(parameters) {
484
485             var layer = getLayer(parseInt(parameters[0]));
486             var cp1x = parseInt(parameters[1]);
487             var cp1y = parseInt(parameters[2]);
488             var cp2x = parseInt(parameters[3]);
489             var cp2y = parseInt(parameters[4]);
490             var x = parseInt(parameters[5]);
491             var y = parseInt(parameters[6]);
492
493             layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y);
494
495         },
496
497         "dispose": function(parameters) {
498             
499             var layer_index = parseInt(parameters[0]);
500
501             // If visible layer, remove from parent
502             if (layer_index > 0) {
503
504                 // Get container element
505                 var layer_container = getLayerContainer(layer_index).getElement();
506
507                 // Remove from parent
508                 layer_container.parentNode.removeChild(layer_container);
509
510                 // Delete reference
511                 delete layers[layer_index];
512
513             }
514
515             // If buffer, just delete reference
516             else if (layer_index < 0)
517                 delete buffers[-1 - layer_index];
518
519             // Attempting to dispose the root layer currently has no effect.
520
521         },
522
523         "distort": function(parameters) {
524
525             var layer_index = parseInt(parameters[0]);
526             var a = parseFloat(parameters[1]);
527             var b = parseFloat(parameters[2]);
528             var c = parseFloat(parameters[3]);
529             var d = parseFloat(parameters[4]);
530             var e = parseFloat(parameters[5]);
531             var f = parseFloat(parameters[6]);
532
533             // Only valid for visible layers (not buffers)
534             if (layer_index >= 0) {
535
536                 // Get container element
537                 var layer_container = getLayerContainer(layer_index).getElement();
538
539                 // Set layer transform 
540                 layer_container.transform(a, b, c, d, e, f);
541
542              }
543
544         },
545  
546         "error": function(parameters) {
547             if (guac_client.onerror) guac_client.onerror(parameters[0]);
548             guac_client.disconnect();
549         },
550
551         "identity": function(parameters) {
552
553             var layer = getLayer(parseInt(parameters[0]));
554
555             layer.setTransform(1, 0, 0, 1, 0, 0);
556
557         },
558
559         "lfill": function(parameters) {
560
561             var channelMask = parseInt(parameters[0]);
562             var layer = getLayer(parseInt(parameters[1]));
563             var srcLayer = getLayer(parseInt(parameters[2]));
564
565             layer.setChannelMask(channelMask);
566
567             layer.fillLayer(srcLayer);
568
569         },
570
571         "line": function(parameters) {
572
573             var layer = getLayer(parseInt(parameters[0]));
574             var x = parseInt(parameters[1]);
575             var y = parseInt(parameters[2]);
576
577             layer.lineTo(x, y);
578
579         },
580
581         "lstroke": function(parameters) {
582
583             var channelMask = parseInt(parameters[0]);
584             var layer = getLayer(parseInt(parameters[1]));
585             var srcLayer = getLayer(parseInt(parameters[2]));
586
587             layer.setChannelMask(channelMask);
588
589             layer.strokeLayer(srcLayer);
590
591         },
592
593         "move": function(parameters) {
594             
595             var layer_index = parseInt(parameters[0]);
596             var parent_index = parseInt(parameters[1]);
597             var x = parseInt(parameters[2]);
598             var y = parseInt(parameters[3]);
599             var z = parseInt(parameters[4]);
600
601             // Only valid for non-default layers
602             if (layer_index > 0 && parent_index >= 0) {
603
604                 // Get container element
605                 var layer_container = getLayerContainer(layer_index);
606                 var layer_container_element = layer_container.getElement();
607                 var parent = getLayerContainer(parent_index).getElement();
608
609                 // Set parent if necessary
610                 if (!(layer_container_element.parentNode === parent))
611                     parent.appendChild(layer_container_element);
612
613                 // Move layer
614                 layer_container.translate(x, y);
615                 layer_container_element.style.zIndex = z;
616
617             }
618
619         },
620
621         "name": function(parameters) {
622             if (guac_client.onname) guac_client.onname(parameters[0]);
623         },
624
625         "png": function(parameters) {
626
627             var channelMask = parseInt(parameters[0]);
628             var layer = getLayer(parseInt(parameters[1]));
629             var x = parseInt(parameters[2]);
630             var y = parseInt(parameters[3]);
631             var data = parameters[4];
632
633             layer.setChannelMask(channelMask);
634
635             layer.draw(
636                 x,
637                 y,
638                 "data:image/png;base64," + data
639             );
640
641             // If received first update, no longer waiting.
642             if (currentState == STATE_WAITING)
643                 setState(STATE_CONNECTED);
644
645         },
646
647         "pop": function(parameters) {
648
649             var layer = getLayer(parseInt(parameters[0]));
650
651             layer.pop();
652
653         },
654
655         "push": function(parameters) {
656
657             var layer = getLayer(parseInt(parameters[0]));
658
659             layer.push();
660
661         },
662  
663         "rect": function(parameters) {
664
665             var layer = getLayer(parseInt(parameters[0]));
666             var x = parseInt(parameters[1]);
667             var y = parseInt(parameters[2]);
668             var w = parseInt(parameters[3]);
669             var h = parseInt(parameters[4]);
670
671             layer.rect(x, y, w, h);
672
673         },
674         
675         "reset": function(parameters) {
676
677             var layer = getLayer(parseInt(parameters[0]));
678
679             layer.reset();
680
681         },
682         
683         "set": function(parameters) {
684
685             var layer = getLayer(parseInt(parameters[0]));
686             var name = parameters[1];
687             var value = parameters[2];
688
689             // Call property handler if defined
690             var handler = layerPropertyHandlers[name];
691             if (handler)
692                 handler(layer, value);
693
694         },
695
696         "shade": function(parameters) {
697             
698             var layer_index = parseInt(parameters[0]);
699             var a = parseInt(parameters[1]);
700
701             // Only valid for visible layers (not buffers)
702             if (layer_index >= 0) {
703
704                 // Get container element
705                 var layer_container = getLayerContainer(layer_index).getElement();
706
707                 // Set layer opacity
708                 layer_container.style.opacity = a/255.0;
709
710             }
711
712         },
713
714         "size": function(parameters) {
715
716             var layer_index = parseInt(parameters[0]);
717             var width = parseInt(parameters[1]);
718             var height = parseInt(parameters[2]);
719
720             // If not buffer, resize layer and container
721             if (layer_index >= 0) {
722
723                 // Resize layer
724                 var layer_container = getLayerContainer(layer_index);
725                 layer_container.resize(width, height);
726
727                 // If layer is default, resize display
728                 if (layer_index == 0) {
729
730                     displayWidth = width;
731                     displayHeight = height;
732
733                     // Update (set) display size
734                     display.style.width = displayWidth + "px";
735                     display.style.height = displayHeight + "px";
736
737                     // Update bounds size
738                     bounds.style.width = (displayWidth*displayScale) + "px";
739                     bounds.style.height = (displayHeight*displayScale) + "px";
740
741                 }
742
743             }
744
745             // If buffer, resize layer only
746             else {
747                 var layer = getBufferLayer(parseInt(parameters[0]));
748                 layer.resize(width, height);
749             }
750
751         },
752         
753         "start": function(parameters) {
754
755             var layer = getLayer(parseInt(parameters[0]));
756             var x = parseInt(parameters[1]);
757             var y = parseInt(parameters[2]);
758
759             layer.moveTo(x, y);
760
761         },
762
763         "sync": function(parameters) {
764
765             var timestamp = parameters[0];
766
767             // When all layers have finished rendering all instructions
768             // UP TO THIS POINT IN TIME, send sync response.
769
770             var layersToSync = 0;
771             function syncLayer() {
772
773                 layersToSync--;
774
775                 // Send sync response when layers are finished
776                 if (layersToSync == 0) {
777                     if (timestamp != currentTimestamp) {
778                         tunnel.sendMessage("sync", timestamp);
779                         currentTimestamp = timestamp;
780                     }
781                 }
782
783             }
784
785             // Count active, not-ready layers and install sync tracking hooks
786             for (var i=0; i<layers.length; i++) {
787
788                 var layer = layers[i].getLayer();
789                 if (layer && !layer.isReady()) {
790                     layersToSync++;
791                     layer.sync(syncLayer);
792                 }
793
794             }
795
796             // If all layers are ready, then we didn't install any hooks.
797             // Send sync message now,
798             if (layersToSync == 0) {
799                 if (timestamp != currentTimestamp) {
800                     tunnel.sendMessage("sync", timestamp);
801                     currentTimestamp = timestamp;
802                 }
803             }
804
805         },
806
807         "transfer": function(parameters) {
808
809             var srcL = getLayer(parseInt(parameters[0]));
810             var srcX = parseInt(parameters[1]);
811             var srcY = parseInt(parameters[2]);
812             var srcWidth = parseInt(parameters[3]);
813             var srcHeight = parseInt(parameters[4]);
814             var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]];
815             var dstL = getLayer(parseInt(parameters[6]));
816             var dstX = parseInt(parameters[7]);
817             var dstY = parseInt(parameters[8]);
818
819             dstL.transfer(
820                 srcL,
821                 srcX,
822                 srcY,
823                 srcWidth, 
824                 srcHeight, 
825                 dstX,
826                 dstY,
827                 transferFunction
828             );
829
830         },
831
832         "transform": function(parameters) {
833
834             var layer = getLayer(parseInt(parameters[0]));
835             var a = parseFloat(parameters[1]);
836             var b = parseFloat(parameters[2]);
837             var c = parseFloat(parameters[3]);
838             var d = parseFloat(parameters[4]);
839             var e = parseFloat(parameters[5]);
840             var f = parseFloat(parameters[6]);
841
842             layer.transform(a, b, c, d, e, f);
843
844         }
845       
846     };
847
848
849     tunnel.oninstruction = function(opcode, parameters) {
850
851         var handler = instructionHandlers[opcode];
852         if (handler)
853             handler(parameters);
854
855     };
856
857
858     this.disconnect = function() {
859
860         // Only attempt disconnection not disconnected.
861         if (currentState != STATE_DISCONNECTED
862                 && currentState != STATE_DISCONNECTING) {
863
864             setState(STATE_DISCONNECTING);
865
866             // Stop ping
867             if (pingInterval)
868                 window.clearInterval(pingInterval);
869
870             // Send disconnect message and disconnect
871             tunnel.sendMessage("disconnect");
872             tunnel.disconnect();
873             setState(STATE_DISCONNECTED);
874
875         }
876
877     };
878     
879     this.connect = function(data) {
880
881         setState(STATE_CONNECTING);
882
883         try {
884             tunnel.connect(data);
885         }
886         catch (e) {
887             setState(STATE_IDLE);
888             throw e;
889         }
890
891         // Ping every 5 seconds (ensure connection alive)
892         pingInterval = window.setInterval(function() {
893             tunnel.sendMessage("sync", currentTimestamp);
894         }, 5000);
895
896         setState(STATE_WAITING);
897     };
898
899     this.scale = function(scale) {
900
901         display.style.transform =
902         display.style.WebkitTransform =
903         display.style.MozTransform =
904         display.style.OTransform =
905         display.style.msTransform =
906
907             "scale(" + scale + "," + scale + ")";
908
909         displayScale = scale;
910
911         // Update bounds size
912         bounds.style.width = (displayWidth*displayScale) + "px";
913         bounds.style.height = (displayHeight*displayScale) + "px";
914
915     };
916
917     this.getScale = function() {
918         return displayScale;
919     };
920
921 };
922
923 /**
924  * Simple container for Guacamole.Layer, allowing layers to be easily
925  * repositioned and nested. This allows certain operations to be accelerated
926  * through DOM manipulation, rather than raster operations.
927  * 
928  * @constructor
929  * 
930  * @param {Number} width The width of the Layer, in pixels. The canvas element
931  *                       backing this Layer will be given this width.
932  *                       
933  * @param {Number} height The height of the Layer, in pixels. The canvas element
934  *                        backing this Layer will be given this height.
935  */
936 Guacamole.Client.LayerContainer = function(width, height) {
937
938     /**
939      * Reference to this LayerContainer.
940      * @private
941      */
942     var layer_container = this;
943
944     // Create layer with given size
945     var layer = new Guacamole.Layer(width, height);
946
947     // Set layer position
948     var canvas = layer.getCanvas();
949     canvas.style.position = "absolute";
950     canvas.style.left = "0px";
951     canvas.style.top = "0px";
952
953     // Create div with given size
954     var div = document.createElement("div");
955     div.appendChild(canvas);
956     div.style.width = width + "px";
957     div.style.height = height + "px";
958
959     /**
960      * Changes the size of this LayerContainer and the contained Layer to the
961      * given width and height.
962      * 
963      * @param {Number} width The new width to assign to this Layer.
964      * @param {Number} height The new height to assign to this Layer.
965      */
966     this.resize = function(width, height) {
967
968         // Resize layer
969         layer.resize(width, height);
970
971         // Resize containing div
972         div.style.width = width + "px";
973         div.style.height = height + "px";
974
975     };
976   
977     /**
978      * Returns the Layer contained within this LayerContainer.
979      * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
980      */
981     this.getLayer = function() {
982         return layer;
983     };
984
985     /**
986      * Returns the element containing the Layer within this LayerContainer.
987      * @returns {Element} The element containing the Layer within this LayerContainer.
988      */
989     this.getElement = function() {
990         return div;
991     };
992
993     /**
994      * The translation component of this LayerContainer's transform.
995      * @private
996      */
997     var translate = "translate(0px, 0px)"; // (0, 0)
998
999     /**
1000      * The arbitrary matrix component of this LayerContainer's transform.
1001      * @private
1002      */
1003     var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity
1004
1005     /**
1006      * Moves the upper-left corner of this LayerContainer to the given X and Y
1007      * coordinate.
1008      * 
1009      * @param {Number} x The X coordinate to move to.
1010      * @param {Number} y The Y coordinate to move to.
1011      */
1012     this.translate = function(x, y) {
1013
1014         // Generate translation
1015         translate = "translate("
1016                         + x + "px,"
1017                         + y + "px)";
1018
1019         // Set layer transform 
1020         div.style.transform =
1021         div.style.WebkitTransform =
1022         div.style.MozTransform =
1023         div.style.OTransform =
1024         div.style.msTransform =
1025
1026             translate + " " + matrix;
1027
1028     };
1029
1030     /**
1031      * Applies the given affine transform (defined with six values from the
1032      * transform's matrix).
1033      * 
1034      * @param {Number} a The first value in the affine transform's matrix.
1035      * @param {Number} b The second value in the affine transform's matrix.
1036      * @param {Number} c The third value in the affine transform's matrix.
1037      * @param {Number} d The fourth value in the affine transform's matrix.
1038      * @param {Number} e The fifth value in the affine transform's matrix.
1039      * @param {Number} f The sixth value in the affine transform's matrix.
1040      */
1041     this.transform = function(a, b, c, d, e, f) {
1042
1043         // Generate matrix transformation
1044         matrix =
1045
1046             /* a c e
1047              * b d f
1048              * 0 0 1
1049              */
1050     
1051             "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")";
1052
1053         // Set layer transform 
1054         div.style.transform =
1055         div.style.WebkitTransform =
1056         div.style.MozTransform =
1057         div.style.OTransform =
1058         div.style.msTransform =
1059
1060             translate + " " + matrix;
1061
1062     };
1063
1064 };
1065
1066 /**
1067  * Map of all Guacamole binary raster operations to transfer functions.
1068  * @private
1069  */
1070 Guacamole.Client.DefaultTransferFunction = {
1071
1072     /* BLACK */
1073     0x0: function (src, dst) {
1074         dst.red = dst.green = dst.blue = 0x00;
1075     },
1076
1077     /* WHITE */
1078     0xF: function (src, dst) {
1079         dst.red = dst.green = dst.blue = 0xFF;
1080     },
1081
1082     /* SRC */
1083     0x3: function (src, dst) {
1084         dst.red   = src.red;
1085         dst.green = src.green;
1086         dst.blue  = src.blue;
1087         dst.alpha = src.alpha;
1088     },
1089
1090     /* DEST (no-op) */
1091     0x5: function (src, dst) {
1092         // Do nothing
1093     },
1094
1095     /* Invert SRC */
1096     0xC: function (src, dst) {
1097         dst.red   = 0xFF & ~src.red;
1098         dst.green = 0xFF & ~src.green;
1099         dst.blue  = 0xFF & ~src.blue;
1100         dst.alpha =  src.alpha;
1101     },
1102     
1103     /* Invert DEST */
1104     0xA: function (src, dst) {
1105         dst.red   = 0xFF & ~dst.red;
1106         dst.green = 0xFF & ~dst.green;
1107         dst.blue  = 0xFF & ~dst.blue;
1108     },
1109
1110     /* AND */
1111     0x1: function (src, dst) {
1112         dst.red   =  ( src.red   &  dst.red);
1113         dst.green =  ( src.green &  dst.green);
1114         dst.blue  =  ( src.blue  &  dst.blue);
1115     },
1116
1117     /* NAND */
1118     0xE: function (src, dst) {
1119         dst.red   = 0xFF & ~( src.red   &  dst.red);
1120         dst.green = 0xFF & ~( src.green &  dst.green);
1121         dst.blue  = 0xFF & ~( src.blue  &  dst.blue);
1122     },
1123
1124     /* OR */
1125     0x7: function (src, dst) {
1126         dst.red   =  ( src.red   |  dst.red);
1127         dst.green =  ( src.green |  dst.green);
1128         dst.blue  =  ( src.blue  |  dst.blue);
1129     },
1130
1131     /* NOR */
1132     0x8: function (src, dst) {
1133         dst.red   = 0xFF & ~( src.red   |  dst.red);
1134         dst.green = 0xFF & ~( src.green |  dst.green);
1135         dst.blue  = 0xFF & ~( src.blue  |  dst.blue);
1136     },
1137
1138     /* XOR */
1139     0x6: function (src, dst) {
1140         dst.red   =  ( src.red   ^  dst.red);
1141         dst.green =  ( src.green ^  dst.green);
1142         dst.blue  =  ( src.blue  ^  dst.blue);
1143     },
1144
1145     /* XNOR */
1146     0x9: function (src, dst) {
1147         dst.red   = 0xFF & ~( src.red   ^  dst.red);
1148         dst.green = 0xFF & ~( src.green ^  dst.green);
1149         dst.blue  = 0xFF & ~( src.blue  ^  dst.blue);
1150     },
1151
1152     /* AND inverted source */
1153     0x4: function (src, dst) {
1154         dst.red   =  0xFF & (~src.red   &  dst.red);
1155         dst.green =  0xFF & (~src.green &  dst.green);
1156         dst.blue  =  0xFF & (~src.blue  &  dst.blue);
1157     },
1158
1159     /* OR inverted source */
1160     0xD: function (src, dst) {
1161         dst.red   =  0xFF & (~src.red   |  dst.red);
1162         dst.green =  0xFF & (~src.green |  dst.green);
1163         dst.blue  =  0xFF & (~src.blue  |  dst.blue);
1164     },
1165
1166     /* AND inverted destination */
1167     0x2: function (src, dst) {
1168         dst.red   =  0xFF & ( src.red   & ~dst.red);
1169         dst.green =  0xFF & ( src.green & ~dst.green);
1170         dst.blue  =  0xFF & ( src.blue  & ~dst.blue);
1171     },
1172
1173     /* OR inverted destination */
1174     0xB: function (src, dst) {
1175         dst.red   =  0xFF & ( src.red   | ~dst.red);
1176         dst.green =  0xFF & ( src.green | ~dst.green);
1177         dst.blue  =  0xFF & ( src.blue  | ~dst.blue);
1178     }
1179
1180 };