-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathhangout.xml
2728 lines (2624 loc) · 85.5 KB
/
hangout.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="AnywhereBoardGames">
<Require feature="rpc"/>
<Require feature="views"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<html>
<head>
<script src="//plus.google.com/hangouts/_/api/v1/hangout.js?v=1.1"></script>
<!--
<script src="//talkgadget.google.com/hangouts/_/api/hangout.js?v=1.1"></script>
<script src="//hangoutsapi.talkgadget.google.com/hangouts/_/api/hangout.js?v=1.1"></script>
-->
<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js" type="text/javascript"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js" type="text/javascript"></script>
<style type="text/css">
#draggable { width: 100px; height: 70px; background: silver; }
.ui-widget-overlay {opacity: 0;}
.popup .ui-dialog-titlebar {display: none; visibility: hidden;}
.popup .ui-button { text-align: left;}
.ui-dialog-content label {width: 20%; display: inline-block; text-align: right;}
*:focus { outline: none; }
</style>
<script type="text/javascript">
// Detection used for Chrome-specific bug fixes
var util_is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
// Bug fix to allow scroll bars to work on mouse down events:
function util_is_in_scrollbar(event){
if ((event.clientX > ($(window).width()-17)) ||
(event.clientY > ($(window).height()-17))){
return (true);
}
return (false);
}
// TODO: IMMEDIATE FIX THIS NAME TO POS AND REVIEW CHANGES
/*
* util_page_to_board_coord - converts page to board coordinates based upon
* the current scrollbar location
*
* @param page The (left, top) page coordinates
* @return board The (left, top) board coordinates
*/
function util_page_to_board_coord(coord){
var left = coord.left;
var top = coord.top;
left += $("#board").scrollLeft();
top += $("#board").scrollTop();
return ({left: left, top: top});
}
/*
* util_board_to_page_coord - converts board to page coordinates based upon
* the current scrollbar location
*
* @param page The (left, top) page coordinates
* @return board The (left, top) board coordinates
*/
function util_board_to_page_coord(coord){
var left = coord.left;
var top = coord.top;
left -= $("#board").scrollLeft();
top -= $("#board").scrollTop();
return ({left: left, top: top});
}
/*
* util_is_touch_event - Helper function to determine if an event is a touch event
*
* @param event The mouse or touch event
*/
function util_is_touch_event(event){
return ((typeof event.touches != 'undefined') || (typeof event.changedTouches != 'undefined'));
}
/*
* util_ignore_click_from_touch - An implementation of the Google "clickbuster"
* to ignore clicks that result from touch events we have already handled
* see http://code.google.com/mobile/articles/fast_buttons.html
*
* Here we try a simpler implementation that ignores clicks for the next 0.5 seconds.
*/
function util_ignore_click_from_touch(){
var ignore_callback = function(event){
event.stopPropagation();
event.preventDefault();
};
var remove_callback = function(){
document.removeEventListener('click',ignore_callback,true);
}
document.addEventListener('click',ignore_callback,true);
setTimeout(remove_callback, 1000);
}
/*
* util_get_event_coordinates - Helper function to get the event coordinates for either a
* mouse or touch event
*
* @param event The mouse or touch event
* @return coord (x, y) coordinates for the event
*/
function util_get_event_coordinates(event){
var coord;
if (util_is_touch_event(event)){
// If this is a touch event, use the first touch
// TODO: LOW - Allow multiple touches and use the closest touch to the object (targetTouches)
if (event.touches.length > 0){
coord = {
x: event.touches[0].pageX,
y: event.touches[0].pageY
};
} else {
coord = {
x: event.changedTouches[0].pageX,
y: event.changedTouches[0].pageY
};
}
} else {
coord = {
x: event.pageX,
y: event.pageY
};
}
return (coord);
}
/*
* util_get_event_board_coordinates - Helper function to get the event coordinates for either a
* mouse or touch event in relation to the top/left of the board
*
* @param event The mouse or touch event
* @return coord (x, y) coordinates for the event
*/
function util_get_event_board_coordinates(event){
var coord = util_get_event_coordinates(event);
return ({x: (coord.x += $("#board").scrollLeft()),
y: (coord.y += $("#board").scrollTop())});
}
/*
* util_clone - Does a shallow clone of an object or array
*
* @param orig Original object or array
*/
function util_clone(orig){
if (orig instanceof Array){
return orig.slice(0);
} else {
return ($.extend({},orig));
}
}
/*
* util_create_ui_overlay - Generates a ui-widget-overlay, sets the window resize
* callback to manipulate it, and returns the DOM object
*
* @param click_callback Callback(event) for click event
* @return ui_overlay DOM object
*/
function util_create_ui_overlay(click_callback){
// Note: We append this to the body to ensure full coverage when the board resizes
var js_overlay = $('<div class="ui-widget-overlay"></div>').css('opacity',0).appendTo('body');
js_overlay.css('position','absolute');
js_overlay.css('z-index',1002);
js_overlay.width($(document).width());
js_overlay.height($(document).height());
var set_overlay_dimension = function (){
js_overlay.width($(document).width());
js_overlay.height($(document).height());
}
$(window).bind("resize",set_overlay_dimension);
js_overlay.bind("click",function(event){
$(window).unbind("resize",set_overlay_dimension);
js_overlay.remove();
if (click_callback){
return (click_callback(event));
}
else {
return true;
}
});
return (js_overlay.get(0))
}
function util_ignore_event(event){
event.preventDefault();
return(false);
}
function util_get_browser_width(){
if( typeof( window.innerWidth ) == 'number' ) {
//Non-IE
return window.innerWidth;
} else if( document.documentElement && document.documentElement.clientWidth ) {
//IE 6+ in 'standards compliant mode'
return document.documentElement.clientWidth;
} else {
//IE 4 compatible
return document.body.clientWidth;
}
}
function util_set_position(piece, position){
$(piece).css({'left': (position.left + "px"), 'top': (position.top + "px")});
}
function util_get_position(piece) {
return ({
'left': parseInt($(piece).css("left"),10),
'top': parseInt($(piece).css("top"),10)
});
}
/*
* Creates a simple pop-up menu.
*
* @param menu_items_config Array of (label, callback, args) to create the menu items
* @param parent DOM object into which the pop-up menu is placed
* @param position Client position (top, left) pair for the placement of the menu
* @param cancel_callback A callback if the menu is closed by clicking the overlay
* @return menu The jQuery dialog object
*/
function create_popup_menu(menu_items_config, parent, position, cancel_callback){
var i, menu, menu_item;
// Create and add the menu items
menu = $('<div style="display:none;"></div>');
// Add to the parent
parent.append(menu);
// Given a menu config, this creates a hander to call the callback and close the dialog
function popup_callback_maker(config){
return function(event) {
menu.dialog('close');
menu.remove();
return config.callback(event,config.args);
};
}
// Now create the menu items with the appropriate callback
for( i in menu_items_config){
menu_item = $('<a href="javascript: void(0);">' +
menu_items_config[i].label + '</a>');
menu_item.num = i;
menu_item.bind('click',popup_callback_maker(menu_items_config[i]));
menu.append(menu_item);
}
// Style the menu
menu.css('padding', 0);
// Turn links unto jquery ui buttons
menu.find('a').css('display','block').button();
// Set up the dialog box
menu.dialog({
dialogClass: 'popup bga_small_text_dialog',
autoOpen: false,
modal: true,
resizable: false,
width: '150',
height: 'auto',
minHeight: 'auto',
minWidth: '150'
});
menu.dialog('option','position',[position.left, position.top]);
// When the dialog opens, set the overlay to close the dialog if you click elsewhere
menu.bind('dialogopen', function(event, ui) {
$('.popup .ui-dialog-titlebar').hide();
$('.ui-widget-overlay').unbind('click');
$('.ui-widget-overlay').css('opacity',0);
// $('.ui-widget-overlay').bind('mousedown touchstart',function(event) {
$('.ui-widget-overlay').bind('click',function(event) {
menu.dialog('close');
menu.remove();
if (cancel_callback){
cancel_callback(event);
}
event.preventDefault();
return false;
});
// Unset the hover that the jquery dialog places on the first item
menu.find('a').removeClass('ui-state-focus ui-state-hover');
});
// Now open the dialog
menu.dialog('open');
return(menu);
}
/* Once thing we don't really have here is an object representing the model of
* the world. Right now we let the piece_ui utilize the piece objects that are
* generated by the DOM to hold all of the model status. If we start allowing
* client side zooming, or other interesting changes, this could be a problem.
* On the other hand, since z-indexing requires special care (to avoid long term
* holes), we'd have to pick up that responsibility.
*
* Another interesting issue is that we have specialized functions for moving,
* locking, etc. Moving was split out so that we could send multiple close
* move commands, and allow the resulting ajax not pile up. Instead we could
* replace almost all of these functions with a general world_piece_model_update
* or even a world_model_update function that could take an array of the changes
* and act accordingly.
*/
// Keep track of the largest index we use for a piece with the server
var world_max_piece_index = -1;
// For now, use the local PHP server to share world data
var world_server_url = "../server/world.php";
// Hold the current local state
var world_local_state = {};
// Allow cross-domain requests in Ajax
$.support.cors = true;
/*
* world_get_new_piece_index - Gets the index of the next piece to be added
* to the world.
*
* TODO: LOW - There is a small race condition if two pieces are added simultaneously
* TODO: LOW - Fill in any null holes from previously deleted pieces first
*/
function world_get_new_piece_index(){
world_max_piece_index ++;
return world_max_piece_index;
}
/*
* world_add_piece - Adds a piece to the world server
*
* @param piece_data Object containing new piece data
*/
function world_add_piece(piece_data){
var piece_index = world_get_new_piece_index();
world_update_piece(piece_index,piece_data);
}
/*
* flatten_recursive_structure - This takes a structured recursive array/object
* and turns it into a single associative array (using "|" to separate
* keys) suitable for use in Google Hangout state
*
* @param update The update to the world
* @param base_key (defaults to "") used for recursion
* @param flat_update (defaults to {}) used for recursion
*/
function flatten_recursive_structure(update, base_key, flat_update){
base_key = (typeof base_key !== 'undefined') ? base_key : "";
flat_update = (typeof flat_update !== 'undefined') ? flat_update : {};
if ($.isArray(update) || $.isPlainObject(update)){
$.each(update, function(k, e){
var new_key = base_key ? (base_key + "|" + k) : k;
if ($.isArray(e) || $.isPlainObject(e)){
flatten_recursive_structure(e, new_key, flat_update);
} else {
if ((e == null) || (e == undefined)){
flat_update[new_key] = "_NULL_";
} else {
flat_update[new_key] = e.toString();
}
}
});
}
return (flat_update);
}
/*
* unflatten_recursive_structure - This a flattened associative array
* and returns it to a structured recursive array/object.
*
* @param flat_update The flattened update
*/
function unflatten_recursive_structure(flat_update){
var update = {};
function compoundkey_set(u, k, v){
k = k.split("|");
var f = k.shift();
while (k.length > 0){
if (!(u[f] instanceof Object)){
// Create object for parent of not there
u[f] = {};
}
u = u[f];
f = k.shift();
}
if (v == "_NULL_"){
u[f] = null;
} else {
u[f] = v;
}
}
// TODO: IMMEDIATE - SLOW DOWN rotate
// TODO: IMMEDIATE - Check Lock and other booleans for string conversion problems
// TODO: MEDIUM - Move Split Code and Split on pieces if possible
// TODO: LOW - Move Split Code really high, time, and pool updates by piece
// TODO: DETERMINE IF WE REALLY NEED TO SORT KEYS IF WE ASSUME EVERYTHING IS AN OBJECT
// Grab the keys
var keys = [];
$.each(flat_update, function(k, v){
keys.push(k);
});
// Now sort the keys
keys.sort();
// Now loop through the keys and setting the update object (so we hit parents before children)
$.each(keys, function(i, k){
compoundkey_set(update, k, flat_update[k]);
});
return (update);
}
/*
* find_deletions - This takes the flat updates, looks for _NULL_, and if found
* locates all of the keys currently in the world to remove
*
* @param flat_update The current flat update
*/
function find_deletions(flat_update){
var deletions = [];
$.each(flat_update, function(k, e){
var child_prefix = k + "|";
if (e == "_NULL_"){ // We found a deletion
$.each(world_local_state, function(ck,ce){
if ((ck.indexOf(child_prefix) == 0)){ // We found a child
deletions.push(ck);
}
});
}
});
return (deletions);
}
// Given a set up submitDelta updates and deletes, this returns true if the update is too big
// This code is updated from hangout.js (thus it is cryptic)
function update_too_big(a,c){
var g=500;
for(b in a) g+=b.length+a[b].length+100;
for(e=0;e<c.length;e++)g+=c[e].length+100;
return(g>1E4);
}
/* world_queue_update - Queues an update to be sent. If we've done an update in the
* in the recent past, we delay the sending of another to avoid hitting Google Hangout's
* throttle
*/
function world_queue_update(updates,deletes){
if (!("queue" in world_queue_update)){
world_queue_update.queue = [];
world_queue_update.last_time = 0;
}
var next_update = {"u": updates,"d": deletes};
world_queue_update.queue.push(next_update);
if (!("loop_running" in world_queue_update)){
world_queue_update.loop_running = true;
var call_next_update = function(){
// Pull the next update
var n = world_queue_update.queue.shift();
// Do we have more updates pending? If so, try to concatenate them
while (world_queue_update.queue.length > 0) {
var n2 = world_queue_update.queue[0];
var combined = {"u": ($.extend({},n["u"],n2["u"])),
"d": n["d"].concat(n2["d"])};
if (update_too_big(combined["u"],combined["d"])) break;
n = combined;
world_queue_update.queue.shift();
}
try {
// Separate deletes from adds (to avoid having a duplicate in both), doing deletes first
if (n["d"].length > 0){
gapi.hangout.data.submitDelta({},n["d"]);
}
gapi.hangout.data.submitDelta(n["u"],[]);
} catch (x) {
console.log(x);
alert('There was an error submitting the world information. Please save your board and reload the application.');
}
world_queue_update.last_time = new Date().getTime();
if (world_queue_update.queue.length > 0){
setTimeout(call_next_update,150);
} else {
// Out of requests so stop
delete world_queue_update.loop_running;
}
}
var now = new Date().getTime();
if ((now - world_queue_update.last_time) < 150){
// If we last updated recently, wait a little
setTimeout(call_next_update,150 - (now - world_queue_update.last_time));
} else {
// Else start now
call_next_update();
}
}
}
/*
* world_update - Sends an update array to the world. Any subsequent calls will be
* combined into a single update until the previous ajax call is completed.
*
* @param update The update to implement in the world
*/
function world_update(update){
var flat_updates = flatten_recursive_structure(update);
var flat_deletions = find_deletions(flat_updates);
// Special case: deleting the world
if (update === 0){
// Increment the new count
var new_count = 1;
if ("__new" in world_local_state){
new_count = Number(world_local_state["__new"] + 1);
}
flat_updates = {"__new": new_count.toString()};
// Remove all keys
flat_deletions = [];
$.each(world_local_state, function(k,e){
if (k != "__new"){
flat_deletions.push(k);
}
});
}
// If our update is too big split it up
if (!update_too_big(flat_updates,flat_deletions)){
world_queue_update(flat_updates,flat_deletions);
} else {
var update_keys = [];
var inc_updates = {}, inc_deletions = [];
var k, l = 0;
// Gather up all update keys
$.each(flat_updates,function(k,v){
update_keys.push(k);
});
// Pull off 15 keys of each type to update
while ((update_keys.length > 0) || (flat_deletions.length > 0)){
if (update_keys.length > 0){
k = update_keys.pop();
inc_updates[k] = flat_updates[k];
l++;
}
if (flat_deletions.length > 0){
inc_deletions.push(flat_deletions.pop());
}
// Send in packs of 15
if ((l > 14) || (inc_deletions.length > 14)){
world_queue_update(inc_updates,inc_deletions);
inc_updates = {};
l = 0;
inc_deletions = [];
}
}
// Send any left
if ((l > 0) || (inc_deletions.length > 0)){
world_queue_update(inc_updates,inc_deletions);
}
}
}
/*
* world_update_piece - Convenience function to update a piece given a piece
* index and an array of attributes
*
* @param piece_index Index of the peice to update
* @param piece_update Object containing the attributes to update
*/
function world_update_piece(piece_index, piece_update){
var update = {
"pieces": new Object()
};
update.pieces[piece_index] = piece_update;
world_update(update);
}
/*
* world_update_piece_accumulate - Accumulates piece updates until
* world_update_piece_accumulate_flush is called. This is useful for easily
* updating many pieces at once. Changes to the same piece will
* completely overwrite old ones.
* ***NOTE*** FOR HANGOUT WE CAN'T ACCUMULATE TOO MUCH, SO WE INTENTIONALLY BREAK THIS
*
* @param piece_index Index of the peice to update
* @param piece_update Object containing the attributes to update
*/
function world_update_piece_accumulate(piece_index, piece_update){
if (!("update" in world_update_piece_accumulate)){
world_update_piece_accumulate.update = {
"pieces": new Object()
};
}
world_update_piece_accumulate.update.pieces[piece_index] = piece_update;
world_update(world_update_piece_accumulate.update);
delete world_update_piece_accumulate.update;
}
/*
* world_update_piece_accumulate_flush - Sends any accumulated piece updates
* gathered in world_update_piece_accumulate() to the server.
* ***NOTE*** FOR HANGOUT WE CAN'T ACCUMULATE TOO MUCH, SO WE INTENTIONALLY BREAK THIS
*/
function world_update_piece_accumulate_flush(){
}
/*
* world_load_from_data - Loads the world from a data object (read from
* a file)
*
* @param data The ABG data file
* @param clear_world Boolean if we should replace the current world
*/
function world_load_from_data(data, clear_world){
// Set the index for our next piece
var next_piece_index = (world_max_piece_index + 1);
// Clear the world
if (clear_world) {
world_update(0);
next_piece_index = 0;
}
// Cycle through pieces and add them
if ("pieces" in data){
$.each(data["pieces"],function(i,p){
// Convert face data to JSON
if ("faces" in p){
p["faces_array"] = JSON.stringify(p["faces"]);
delete p["faces"];
}
// Make sure it appears
p["client_id"] = -1;
world_update_piece(next_piece_index,p);
next_piece_index++;
});
} else {
alert('Sorry, but the URL you provided did not contain any pieces');
}
}
/*
* world_load_from_url - Uses Ajax to read the contents of an ABG file
* and either adds it or replaces the current world to it
*
* @param url The URL for the Word
* @param clear_world Boolean if we should replace the current world
*/
function world_load_from_url(url, clear_world){
var world_load_failure = function(data, textStatus, errorThrown){
alert("Sorry, we were unable to read the board game data. (Please note that loading a board from a URL is not supported in IE9.)")
}
var world_load_handler = function(data){
try {
data = JSON.parse(data);
} catch (x) {
alert('The provided URL does not contain valid board game data.');
return;
}
world_load_from_data(data, clear_world);
}
$.ajax({
url: url,
success: world_load_handler,
error: world_load_failure,
dataType: "text"
});
}
/*
* world_load_from_file - Uses FileReader to read the contents of an ABG file
* and either adds it or replaces the current world to it
*
* @param file The file object
* @param clear_world Boolean if we should replace the current world
*/
function world_load_from_file(file, clear_world){
if (window.FileReader === undefined){
alert("Sorry, but your browser does not support client-side file reading. " +
"Please consider getting the latest version of Chrome or Firefox");
}
var file_reader = new FileReader();
file_reader.onError = function(){
alert("Sorry, we were unable to read the file.")
}
file_reader.onload = function(evt){
var data;
try {
data = JSON.parse(evt.target.result);
} catch (x) {
alert('The provided file does not contain valid board game data.');
return;
}
world_load_from_data(data, clear_world);
}
file_reader.readAsText(file);
}
/* world_save_world - Saves the current world to a file (in a new window)
*/
function world_save_world(){
var pieces = [];
$.each(g_pieces,function(i,piece){ // Note g_pieces is in piece_ui.js
var offset = util_page_to_board_coord($(piece).offset());
pieces.push({
"faces": piece.faces, // Note: for download we keep the array as is (not JSON)
"face_width": piece.face_width,
"x": (offset.left),
"y": (offset.top),
"z": piece.z,
"lock": piece.lock,
"shield": piece.shield,
"orientation": piece.orientation,
"face_showing": piece.face_showing,
"css_class": piece.css_class,
"event_callback": piece.event_callback,
"custom_html": escape(piece.custom_html)
});
});
var world = { "_new":1, "pieces": pieces};
try {
window.open('data:text/json;filename=board.abg,' +
encodeURIComponent(JSON.stringify(world)),
'board.abg');
} catch (x) {
alert("Sorry, we are unable to save the board. (Please note that saving board state is not supported in IE9.)");
}
}
/*
* world_on_new_piece_handler - This is a handler function(piece_index, piece_data)
* that is set by the code interested in listening to piece additions to the world
* When a new piece is added, the piece_index is set to the index used by the world
* to reference changes (the index for the change handler in
* world_on_piece_change_handlers) and piece_data is an array holding any changed
* data for the piece.
*/
var world_on_new_piece_handler = function(){};
/*
* world_on_piece_change_handlers - This is an array of change handlers
* function(piece_data) that is set by the code interested in listening
* to piece changes. The array is indexed by the piece_index (see
* world_on_new_piece_handler).
*/
var world_on_piece_change_handlers = {};
function execute_world_update(update){
var piece_index;
// Handle a new world
if ((!(update instanceof Object)) || ("__new" in update)) {
// Reset max piece index
world_max_piece_index = -1;
// Delete existing pieces
for (piece_index in world_on_piece_change_handlers){
world_on_piece_change_handlers[piece_index](null);
// Unregister the handler
delete world_on_piece_change_handlers[piece_index];
}
// Now add new pieces
if ((update instanceof Object) && ("pieces" in update)){
for (piece_index in update.pieces) {
if (Number(piece_index) > world_max_piece_index){
world_max_piece_index = Number(piece_index);
}
// Add the piece if it isn't null
if (update.pieces[piece_index] instanceof Object){
world_on_new_piece_handler(piece_index, update.pieces[piece_index]);
}
}
}
} else if ("pieces" in update) {
// Iterate pieces, looking for new, updates, or deletes
for (piece_index in update.pieces) {
if ((update.pieces[piece_index] instanceof Object) &&
(!(Number(piece_index) in world_on_piece_change_handlers))) {
if (Number(piece_index) > world_max_piece_index){
world_max_piece_index = Number(piece_index);
}
world_on_new_piece_handler(piece_index, update.pieces[piece_index]);
} else if (piece_index in world_on_piece_change_handlers){
world_on_piece_change_handlers[piece_index](update.pieces[piece_index]);
// Check if the piece was deleted
if (update.pieces[piece_index] === null){
// Unregister the handler
delete world_on_piece_change_handlers[piece_index];
}
}
}
}
}
/*
* world_listener_start - Implements an loop that checks for updates from
* the world server. It calls "execute_world_update" if there is an update.
*/
function world_listener_start(){
// When the state is updated, this handles the update data
var world_update_handler = function(eventObj){
var flat_update = {};
var i, k, v;
for (i = 0; i < eventObj.addedKeys.length; ++i){
k = eventObj.addedKeys[i].key;
v = eventObj.addedKeys[i].value;
flat_update[k] = v;
world_local_state[k] = v;
}
for (i = 0; i < eventObj.removedKeys.length; ++i){
k = eventObj.removedKeys[i].key;
delete world_local_state[k];
}
var update = unflatten_recursive_structure(flat_update);
// console.log(JSON.stringify(update));
execute_world_update(update);
}
// Get the initial state
world_local_state = gapi.hangout.data.getState();
if (world_local_state){
var update = unflatten_recursive_structure(world_local_state);
// console.log(JSON.stringify(update));
execute_world_update(update);
// See if the world is empty
if (!("pieces" in update)){
// See if data was passed in
var appData = gadgets.views.getParams()['appData'];
if (appData){
world_load_from_url(appData,1);
} else {
world_load_from_url('https://res.anywhereboardgames.com/abg/intro.abg.txt',1);
}
}
}
// Register our update andler
gapi.hangout.data.onStateChanged.add(world_update_handler);
}
// Start the world listener
gapi.hangout.onApiReady.add(function(eventObj){
try {
if (eventObj.isApiReady){
world_listener_start();
}
} catch (x) {
alert("Sorry... there was a problem initializing the board. Please reload the application (and make sure you are using an updated version of your browser). ")
console.log(x);
}
});
/*
* piece_ui.js is responsible for displaying and allowing the user to
* manipulate the pieces on the board.
*
* It requires:
* - The world javascript functions (world.js)
* - A #board defined in HTML to add elements
*
* NOTE ON BACKGROUND CLICK-AND-DRAG:
* With this code, we work hard to support both mouse and touch events. In
* most cases, single touch events are treated identically to the mouse
* down and move events with one key exception: what to do when the background
* or locked pieces are dragged. For touch devices, users expect the screen
* to pan and pinches to zoom. For mouse devices, users expect multi-select
* to occur when the background or locked pieces are clicked and dragged.
*/
/*
* g_pieces - This array holds all of the pieces on the board. This is useful
* for multi-select collision determiniation and z-index maintenance (to avoid
* gaps in the z-index when a piece is moved to the bottom or top).
*/
var g_pieces = [];
/*
* g_client_id - A unique client ID that can be used to ignore update messages
* that we generated and have already displayed (like piece move and rotate)
*
* TODO: LOW - Make the client ID truly unique (currently low probability of hitting another client)
*/
var g_client_id = (""+Math.random()).split(".").pop();
/*
* g_is_touch_device - if we encounter a touch device, we know to ignore some
* keey events. This is set when we encounter a touch event
*/
var g_is_touch_device = 0;
/**
* on_new_piece_handler - Callback for when a new piece is added to the world
*
* @param piece_idx The index of the piece in the world (needed for world update calls)
* @param piece_data Data about the newly added piece
*/
function on_new_piece_handler(piece_idx, piece_data){
var source = "";
// Set some defaults if not there (this allows pieces to be split across multiple updates)
if (!("z" in piece_data)){
piece_data.z = 0;
}
if (!("x" in piece_data)){
piece_data.x = 0;
}
if (!("y" in piece_data)){
piece_data.y = 0;
}
if (!("face_showing" in piece_data)){
piece_data.face_showing = 0;
}
if (!("faces_array" in piece_data)){
piece_data.faces = [];
} else {
piece_data.faces = JSON.parse(piece_data.faces_array);
source = piece_data.faces[piece_data.face_showing];
}
// TODO: Handle both "faces" and "faces_array" for backwards compatibility with save files
// Unparse the face array
// Create piece HTML in the proper location
var jq_piece = $('<span class="piece" id="piece_' + piece_idx +
'" style="position: absolute; left: ' + piece_data.x + 'px; top: ' + piece_data.y + 'px;">' +
'<img class="piece_face" src="' + source + '">' +
'<div class="custom_html"></div></span>');
// Add to the board
$("#board").append(jq_piece);
// Get the object for the piece itself
var piece = jq_piece.get(0);
// Add the piece to our global list
g_pieces.push(piece);
// Record the piece index into the piece object
piece.world_piece_index = piece_idx;
// Record the lock state into the piece object
piece.lock = Number(piece_data.lock) ? piece_data.lock : 0;
// Record if the piece is a shield
piece.shield = Number(piece_data.shield) ? piece_data.shield : 0;
// Record the faces
piece.faces = piece_data.faces;
// Set the piece face's width
piece.face_width = "";
if ("face_width" in piece_data){
piece.face_width = piece_data.face_width;
if (Number(piece_data.face_width) > 0){
$(piece).find('.piece_face').attr('width',piece.face_width);
} else {
$(piece).find('.piece_face').removeAttr('width');
}
}
// Initialize the z index
set_piece_z_index(piece, Number(piece_data.z));
// Set the face
set_piece_face_showing(piece,piece_data.face_showing);
// Record the orientation
piece.orientation = piece_data.orientation ? Number(piece_data.orientation) : 0;
set_piece_orientation(piece,piece.orientation);
// Add the move handler
$(piece).bind({
mousedown: on_piece_touch_start
});
// Add mouse touch event (for mobile devices)
if (piece.addEventListener){
piece.addEventListener("touchstart",on_piece_touch_start,false);
}
// Set the piece CSS classes
piece.css_class = piece_data.css_class ? piece_data.css_class : "";
if (piece.css_class){
$(piece).addClass(piece.css_class);
}
// Set the piece callback
piece.event_callback = piece_data.event_callback ? piece_data.event_callback : "";
// Set up custom HTML
piece.custom_html = "";
if ("custom_html" in piece_data){
piece.custom_html = unescape(piece_data.custom_html);
$(piece).find('.custom_html').html(piece.custom_html);
}
// Set up change handler for piece
world_on_piece_change_handlers[piece_idx] = function(piece_data){
if (piece_data === null){
// Remove the piece from our global list
g_pieces.splice($.inArray(piece,g_pieces),1);
// If piece_data is null, then the piece is removed, so get rid of it
$(piece).remove();
} else {
if ("client" in piece_data){
piece.client = piece_data.client;
}
// Move the piece (as long as we didn't move it initially)
if (("x" in piece_data) && ("y" in piece_data)){
if (piece.client != g_client_id) {
util_set_position(piece,{
left: Number(piece_data.x),
top: Number(piece_data.y)
});
}
}
// Rotate the piece (as long as we didn't rotate it initially)
if ("orientation" in piece_data){
if (piece.client != g_client_id) {
piece.orientation = Number(piece_data.orientation);
set_piece_orientation(piece,piece.orientation);
}
}
// Set the shield status
if ("shield" in piece_data){
piece.shield = Number(piece_data.shield);