summaryrefslogtreecommitdiffstats
path: root/modules-available/roomplanner/js/lib/jquery-ui-draggable-collision.js
blob: 3ef553b1412b0bf140fe48ed7d656b46bdb226ee (plain) (blame)
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
/*
Copyright (c) 2014 Djuri Baars
Copyright (c) 2011 Sean Cusack

MIT-LICENSE:

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

(function($){
	// Default settings
	var DEBUG = false;
	var VISUAL_DEBUG = DEBUG;

	$.ui.draggable.prototype.options.obstacle          = ".ui-draggable-collision-obstacle";
	$.ui.draggable.prototype.options.restraint         = ".ui-draggable-collision-restraint";
	$.ui.draggable.prototype.options.collider          = ".ui-draggable-dragging";
	$.ui.draggable.prototype.options.colliderData      = null;
	$.ui.draggable.prototype.options.obstacleData      = null;
	$.ui.draggable.prototype.options.directionData     = null;
	$.ui.draggable.prototype.options.relative          = "body";
	$.ui.draggable.prototype.options.preventCollision  = false;
	$.ui.draggable.prototype.options.preventProtrusion = false;
	$.ui.draggable.prototype.options.collisionVisualDebug = false;
	$.ui.draggable.prototype.options.multipleCollisionInteractions = [];

	// Plugin setup
	$.ui.plugin.add( "draggable", "obstacle", {
		create: function(event,ui){       handleInit   .call( this, event, ui ); },
		start: function(event,ui){        handleStart  .call( this, event, ui ); } ,
		drag:  function(event,ui){ return handleCollide.call( this, event, ui ); } ,
		stop:  function(event,ui){        handleCollide.call( this, event, ui );
																			handleStop   .call( this, event, ui ); }
	});

	// NOTE: the "handleCollide" function must do all collision and protrusion detection at once, in order for the
	//       simultaneous prevention cases to work properly, so basically, if you ask for both, the obstacle events
	//       will occur first (and do both), and then these will trigger, see that they have an obstacle, and not
	//       do anything a second time
	$.ui.plugin.add( "draggable", "restraint", {
		create: function(event,ui){         handleInit   .call( this, event, ui ); },
		start: function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle ) // if there are obstacles, we already handled both
															 {
																				handleStart  .call( this, event, ui );
															 }
														 } ,
		drag:  function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle ) // if there are obstacles, we already handled both
															 {
																 return handleCollide.call( this, event, ui );
															 }
														 } ,
		stop:  function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle ) // if there are obstacles, we already handled both
															 {
																				handleCollide.call( this, event, ui );
																				handleStop   .call( this, event, ui );
															 }
														 }
	});

	// Likewise, if we already have an obstacle or restraint, we've done it all, so don't repeat
	$.ui.plugin.add( "draggable", "multipleCollisionInteractions", {
		create: function(event,ui){         handleInit   .call( this, event, ui ); },
		start: function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle &&
																	 ! $(this).data("ui-draggable").options.restraint   )
															 {
																				handleStart  .call( this, event, ui );
															 }
														 } ,
		drag:  function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle &&
																	 ! $(this).data("ui-draggable").options.restraint   )
															 {
																 return handleCollide.call( this, event, ui );
															 }
														 } ,
		stop:  function(event,ui){ if( ! $(this).data("ui-draggable").options.obstacle &&
																	 ! $(this).data("ui-draggable").options.restraint   )
															 {
																				handleCollide.call( this, event, ui );
																				handleStop   .call( this, event, ui );
															 }
														 }
	});


	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Private Classes
	//

	////////////
	// EVENTS //
	////////////

	function CollisionEvent( eventType, collider, obstacle, collisionType, collision )
	{
		jQuery.Event.call( this, eventType );
		this.collider  = collider;
		this.obstacle  = obstacle;
		this.collisionType = collisionType;
		this.collision = collision;
	}

	CollisionEvent.prototype = new $.Event( "" );

	function CollisionCheckEvent( eventType, collider, obstacle, collisionType )
	{
		jQuery.Event.call( this, eventType );
		this.collider  = collider;
		this.obstacle  = obstacle;
		this.collisionType = collisionType;
	}

	CollisionCheckEvent.prototype = new $.Event( "" );

	//////////////////////
	// COORDINATE CLASS //
	//////////////////////

	function Coords( x1, y1, x2, y2 )
	{
		this.x1 = x1;
		this.y1 = y1;
		this.x2 = x2;
		this.y2 = y2;
	}

	Coords.prototype.width   = function() { return (this.x2-this.x1);   };
	Coords.prototype.height  = function() { return (this.y2+this.y1);   };
	Coords.prototype.centerx = function() { return (this.x1+this.x2)/2; };
	Coords.prototype.centery = function() { return (this.y1+this.y2)/2; };
	Coords.prototype.area    = function() { return this.width()*this.height(); };
	Coords.prototype.hash    = function() { return "["+[this.x1,this.y1,this.x2,this.y2].join(",")+"]"; };
	Coords.prototype.distance = function(c)
	{
		return this.distanceTo( c.centerx(), c.centery() );
	};

	Coords.prototype.distanceTo = function(x,y)
	{
		var dx = this.centerx()-x;
		var dy = this.centerx()-y;
		return Math.sqrt( dx*dx + dy*dy );
	};

	/////////////////////////////////
	// COORDINATE HELPER FUNCTIONS //
	/////////////////////////////////

	// create a box with the same total area, centered at center of gravity
	function centerGravity( coordsList )
	{
		if( coordsList.length <= 0 ) return null;
		var wsumx = 0;
		var wsumy = 0;
		var suma  = 0;
		for( var i = 0; i < coordsList.length; i++ )
		{
			suma += coordsList[i].area();
			wsumx += coordsList[i].centerx() * coordsList[i].area();
			wsumy += coordsList[i].centery() * coordsList[i].area();
		}
		var d = Math.sqrt( suma ); // dimension of square (both w and h)
		return new Coords( (wsumx/suma) - d/2, (wsumy/suma) - d/2, (wsumx/suma) + d/2, (wsumy/suma) + d/2 );
	}

	// convert a jq object into a Coords object, handling all the nice-n-messy offsets and margins and crud
	function jq2Coords( jq, dx, dy )
	{
		var x1,y1, x2, y2;

		if( !dx ) dx=0;
		if( !dy ) dy=0;
		if( jq.parent().length > 0 )
		{
			x1 = dx + jq.offset().left - (parseInt(jq.css("margin-left"))||0);
			y1 = dy + jq.offset().top  - (parseInt(jq.css("margin-top" ))||0);
			x2 = x1 + jq.outerWidth( true);
			y2 = y1 + jq.outerHeight(true);
		} else {
			x1 = dx + parseInt(jq.css("left"  )) || 0;
			y1 = dy + parseInt(jq.css("top"   )) || 0;
			x2 = x1 + parseInt(jq.css("width" )) || 0;
			y2 = y1 + parseInt(jq.css("height")) || 0;
			x2 += (parseInt(jq.css("margin-left"))||0) + (parseInt(jq.css("border-left"))||0) + (parseInt(jq.css("padding-left"))||0) +
						(parseInt(jq.css("padding-right"))||0) + (parseInt(jq.css("border-right"))||0) + (parseInt(jq.css("margin-right"))||0);
			y2 += (parseInt(jq.css("margin-top"))||0) + (parseInt(jq.css("border-top"))||0) + (parseInt(jq.css("padding-top"))||0) +
						(parseInt(jq.css("padding-bottom"))||0) + (parseInt(jq.css("border-bottom"))||0) + (parseInt(jq.css("margin-bottom"))||0);

		}
		return new Coords( x1, y1, x2, y2 );
	}

	function jqList2CenterGravity( jqList, dx, dy )
	{
		return centerGravity( jqList.toArray().map( function(e,i,a){ return jq2Coords($(e),dx,dy); } ) );
	}

	/////////////////////
	// COLLISION CLASS //
	/////////////////////

	function Collision( jq, cdata, odata, type, dx, dy, ddata, recentCenterOfGravity, mousex, mousey )
	{
		if(!recentCenterOfGravity) recentCenterOfGravity=jqList2CenterGravity($(this.collider), dx, dy);
		if(!dx) dx = 0;
		if(!dy) dy = 0;
		this.collision = $(jq            );
		this.collider  = $(jq.data(cdata));
		this.obstacle  = $(jq.data(odata));
		this.direction =   jq.data(ddata);
		this.type      = type;
		this.dx        = dx;
		this.dy        = dy;
		this.centerOfMass    = recentCenterOfGravity;
		this.collisionCoords = jq2Coords( this.collision );
		this.colliderCoords  = jq2Coords( this.collider,  dx, dy );
		this.obstacleCoords  = jq2Coords( this.obstacle  );
		if(!mousex) mousex = this.colliderCoords.centerx();
		if(!mousey) mousex = this.colliderCoords.centery();
		this.mousex = mousex;
		this.mousey = mousey;
	}

	// amount "embedded" into obstacle in x-direction
	// might be negative or zero if it doesn't make sense
	// this is used with the delta calculation - if its <= 0, it'll get skipped
	// dirx is -1 or +1, depending on which way we are orienting things (which way we want to move it)
	// NOTE: originally, we were taking the collision area into account, but it's easier to recalc embed value
	Collision.prototype.embedx = function( dirx )
	{
		if( this.type == "collision" )
		{
			if( dirx < 0 ) /* want to move left  */ return this.colliderCoords.x2 - this.obstacleCoords.x1;
			if( dirx > 0 ) /* want to move right */ return this.obstacleCoords.x2 - this.colliderCoords.x1;
		}
		else if( this.type == "protrusion" )
		{
			// if we're embedded in a top/bottom edge, don't move left or right, silly:
			if( ( this.direction == "N" ) || ( this.direction == "S" ) ) return 0;

			if( dirx < 0 ) /* want to move left  */ return this.colliderCoords.x2 - this.obstacleCoords.x2;
			if( dirx > 0 ) /* want to move right */ return this.obstacleCoords.x1 - this.colliderCoords.x1;
		}
		return 0;
	};

	// and ditto for y-direction
	Collision.prototype.embedy = function( diry )
	{
		if( this.type == "collision" )
		{
			if( diry < 0 ) /* want to move up   */ return this.colliderCoords.y2 - this.obstacleCoords.y1;
			if( diry > 0 ) /* want to move down */ return this.obstacleCoords.y2 - this.colliderCoords.y1;
		}
		else if( this.type == "protrusion" )
		{
			// if we're embedded in a left/right edge, don't move up or down, silly:
			if( ( this.direction == "E" ) || ( this.direction == "W" ) ) return 0;

			if( diry < 0 ) /* want to move up   */ return this.colliderCoords.y2 - this.obstacleCoords.y2;
			if( diry > 0 ) /* want to move down */ return this.obstacleCoords.y1 - this.colliderCoords.y1;
		}
		return 0;
	};

	// distance from collision to recent center of mass, i.e. it used to be in one place, and we're dragging it
	// to another, so the "overlap" of some collider happens a certain "distance" from the center of where stuff
	// used to be...
	Collision.prototype.distance = function()
	{
		var cx1 = this.centerOfMass.centerx();
		var cy1 = this.centerOfMass.centery();
		var cx2 = this.collisionCoords.centerx();
		var cy2 = this.collisionCoords.centery();
		return Math.sqrt( (cx2-cx1)*(cx2-cx1) + (cy2-cy1)*(cy2-cy1) );
	};

	Collision.prototype.hash = function(){ return this.type+"["+this.colliderCoords.hash()+","+this.obstacleCoords.hash()+"]"; };

	////////////////////////////////
	// COLLISION HELPER FUNCTIONS //
	////////////////////////////////

	// sort so that collisions closest to recent center of mass come first -- we need to resolve them in order
	function collisionComparison(c1,c2)
	{
		var cd1 = c1.distance();
		var cd2 = c2.distance();
		return ( ( cd1 < cd2 ) ? -1 : ( cd1 > cd2 ) ? +1 : 0 );
	}

	///////////////////////
	// INTERACTION CLASS //
	///////////////////////

	function Interaction( draggable, options )
	{
		this.draggable         = $(draggable);
		this.obstacleSelector  =   options.obstacle        || ".ui-draggable-collision-obstacle"   ;
		this.restraintSelector =   options.restraint       || ".ui-draggable-collision-restraint"  ;
		this.obstacle          = $(options.obstacle        || ".ui-draggable-collision-obstacle"  );
		this.restraint         = $(options.restraint       || ".ui-draggable-collision-restraint" );
		var  collider          =   options.collider        || ".ui-draggable-dragging"             ;
		this.collider          = draggable.find( collider ).andSelf().filter( collider );
		this.colliderData      = options.colliderData      || null;
		this.obstacleData      = options.obstacleData      || null;
		this.directionData     = options.directionData     || null;
		this.relative          = options.relative          || "body";
		this.preventCollision  = options.preventCollision  || false;
		this.preventProtrusion = options.preventProtrusion || false;
		this.collisions        = $();
		this.protrusions       = $();
	}

	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// Main handler functions
	//

	function uiTrigger( _this, widget, eventName, event, ui )
	{
		$.ui.plugin.call( widget, eventName, event, ui );
		_this.trigger( event, ui );
	}

	function handleInit( event, ui, type )
	{
		var w = $(this).data("ui-draggable");
		var o = w.options;
	}

	function handleStart(event,ui)
	{
		VISUAL_DEBUG = $(this).data("ui-draggable").options.collisionVisualDebug;
		$(this).data( "jquery-ui-draggable-collision-recent-position", ui.originalPosition );
	}

	function handleStop (event,ui)
	{
		$(this).removeData("jquery-ui-draggable-collision-recent-position");
		if( VISUAL_DEBUG ) $(".testdebug").remove();
		VISUAL_DEBUG = DEBUG;
	}

	// This is the monolithic workhorse of the plugin:
	//   * At the beginning and end, it sends out all the pre/post-collision/protrusion events
	//   * In the middle, it both calculates collisions, and prevents them if requested
	//   * When it's either tried its best, or found a fit, or wasn't required to avoid obstacles, it sends out actual collision events
	//
	// Inside the first big loop is the actual "prevention" logic
	//   * It calculates the "intended position" of everything, checks all the collision logic, and if it needs to,
	//     then calculates a delta movement to see if that fits, and the loop continues until it either works,
	//     or an arbitrary iteration limit is reached, just in case it gets in a loop
	//   * The delta function is described in more detail below
	//
	// During all the "trying a new position" and "determining collisions" calculations, it's not using purely the
	//   current position of the colliders -- it can't because the draggable is now in a new "intended position",
	//   and with it, all its children, including any collider children
	//   * So, it keeps track of a dx and dy from known position, and populates a "jquery-collision-coordinates" data value
	//     that the jquery-collision plugin takes into account during the calculations
	//   * Also, the Coords() values get populated with these offsets at various times, so that they reflect "intended position"
	//
	// Note also that the collider, obstacle, and direction data fields are temporarily overriden (because we need them here,
	//   and the user may not have asked for them), and then erased and placed where the user wants them, right before
	//   sending out the collision events
	//
	// Note also that the collisions and protrusions requested are "relative" to "body". If the use asked for
	// something relative, it has to get translated right before sending out the events...
	function handleCollide( event, ui )
	{
		// Note that $(this) is the draggable that's moving - it has a ui.position that moves acording to where
		// the draggable is "about to move". However, our "collidable" objects might not be the same as $(this) -
		// they might be child elements. So we need to keep track of recent and present position so we can apply the
		// "intended" dx and dy to all the moving elements:
		var rp = $(this).data("jquery-ui-draggable-collision-recent-position");

		if( DEBUG ) { console.log( "handleCollision ******************************************************************" ); }

		if( VISUAL_DEBUG ) $(".testdebug").remove();

		var ctyp  =     "collision";
		var prec  =  "precollision";
		var postc = "postcollision";
		var ptyp  =     "protrusion";
		var prep  =  "preprotrusion";
		var postp = "postprotrusion";

		// NOTE: widget is used for uiTrigger, otherwise event-binders don't get a "ui" variable
		var widget = $(this).data("ui-draggable");
		var o      = widget.options;

		// List of Interactions -- first one is the main set of args from the .draggable() setup call, rest are multipleCollisionInteractions:[...]
		var ilist  = [];

		if( o.obstacle || o.restraint ) ilist.push( new Interaction( $(this), o ) );
		if( o.multipleCollisionInteractions && o.multipleCollisionInteractions['length'] )
		{
			var mci = o.multipleCollisionInteractions;
			for( var i=0; i<mci.length; i++ )
				ilist.push( new Interaction( $(this), mci[i] ) );
		}

		if( ilist.length <= 0 )
		{
			// Just forget the whole stpuid business - why are we in here, anyways - no interactions to check
			// Cache the current position anyways, and jump out:
			$(this).data( "jquery-ui-draggable-collision-recent-position", ui.position );
			return;
		}

		var d1 = "ui-draggable-collision-collider-temp";
		var d2 = "ui-draggable-collision-obstacle-temp";
		var d3 = "ui-draggable-collision-direction-temp";
		var di = "ui-draggable-collision-interaction-temp";
		var d  = event.data;
		var as = "<div />";
		if( d && d.as ) as = d.as;

		var e;

		// Try moving things twice the number of colliders times obstacles+restraints, plus 1 original attempt
		// We need to calculate maxiter here because there may be several levels of interactions
		// (Honestly, anything will do, just need a reasonable cutoff)
		var maxiter = 1;

		// Global just-about-to-check-collisions event:
		for( var i=0; i<ilist.length; i++ )
		{
			maxiter += 2 * ilist[i].collider.length * ( ilist[i].obstacle.length + ilist[i].restraint.length );

			if( VISUAL_DEBUG )
			{
				ilist[i].obstacle .each(function(){$(this).clone().removeClass().empty().addClass("testdebug").css("pointer-events","none")
																									.css("background","transparent").css("padding","0px")
																									.css("border","1px solid black"  ).css("margin","-1px")
																									.appendTo($(this).parent());});
				ilist[i].restraint.each(function(){$(this).clone().removeClass().empty().addClass("testdebug").css("pointer-events","none")
																									.css("background","transparent").css("padding","0px")
																									.css("border","1px solid magenta").css("margin","-1px")
																									.appendTo($(this).parent());});
			}

			if( ilist[i].obstacleSelector )
			{
				e = new CollisionCheckEvent( prec, $(this), ilist[i].obstacle, ctyp )
				uiTrigger( ilist[i].collider, widget, prec, e, ui );
			}

			if( ilist[i].restraintSelector )
			{
				e = new CollisionCheckEvent( prep, $(this), ilist[i].restraint, ptyp )
				uiTrigger( ilist[i].collider, widget, prep, e, ui );
			}
		}

		var origleft = rp.left;
		var origtop  = rp.top;
		var origdx = ui.position.left - rp.left;
		var origdy = ui.position.top  - rp.top;
		var dx;
		var dy;
		var ocl  = []; // list of Collision()'s total
		var cocl = []; // list of Collision()'s just from collisions
		var pocl = []; // list of Collision()'s just from protrusions
		var ccl  = []; // list of Collision()'s just from containment (also a protrusion)

		// Check if there's containment for the draggable:
		var $cont = $();
		if( widget.containment )
		{
			maxiter += 2; // the main draggable container times twice the number of containers (1)

			var cn = widget.containment;
			$cont = $("<div />").offset( { left: cn[0], top: cn[1] } )
													.width(  cn[2]-cn[0]+$(this).width( ) )  // because it had the draggable's size chopped out :-P
													.height( cn[3]-cn[1]+$(this).height() ); // because it had the draggable's size chopped out :-P

			if( VISUAL_DEBUG )
				$cont.clone()
						 .css("background","transparent")
						 .css("border","1px solid blue")
						 .css("margin","-1px").css("padding","0px")
						 .addClass("testdebug")
						 .appendTo();
		}

		var deltaCache = {};
		var iter = 0;
		while( iter < maxiter )
		{
			iter++;

			// Calc offset from recent move, so we can move the objects that are "coming along for the ride" before calculating their
			// collisions. Otherwise the ui variable only keeps track of the main draggable, not its contents, which may contain
			// the actual things that collide
			dx = ui.position.left - rp.left;
			dy = ui.position.top  - rp.top;

			// Empty the collision containers outside the interaction loop:
			ocl = [];
			cocl = [];
			pocl = [];

			for( var i=0; i<ilist.length; i++ )
			{
				ilist[i].collisions  = $();
				ilist[i].protrusions = $();

				var $c = ilist[i].collider;
				var $o = ilist[i].obstacle;
				var $r = ilist[i].restraint;
				if( DEBUG ) console.log( "trying inter,c,o,r=",ilist[i],$c,$o,$r)

				// Add offset to coordinates before figuring out collisions, because we're basing it on "where its about to go", not "where it is":
				// (Don't do this for anything but colliders! Applying to obstacles or restrictions or containment screws things up!!)
				$c.each( function(){ $(this).data( "jquery-collision-coordinates", jq2Coords($(this),dx,dy) ); } );

				var cog = jqList2CenterGravity($c);
				for( var ci=0; ci<$c.length; ci++ )
				{
					// Calculate collisions separately from protrusions, as we might only prevent one or the other:
					var oc = $($c[ci]).collision( $o, { mode: "collision",  as: "<div />", colliderData: d1, obstacleData: d2, directionData: d3, relative: "body" } );
					if( DEBUG ) { console.log( "collisions", oc ); }

					// Add the interaction settings to their data, so we can pick it apart later:
					oc.data( di, ilist[i] );

					// And add the collisions to the interaction:
					ilist[i].collisions = ilist[i].collisions.add(oc);

					// And if there are any, make the appropriate Collision() objects and add them to the list
					if( oc.length > 0 )
					{
						cocl = oc.toArray().map( function(e,i,a){ return new Collision($(e), d1, d2, "collision" , dx, dy, d3, cog, event.pageX, event.pageY ); } );
						ocl = ocl.concat( cocl );
						if(VISUAL_DEBUG) $("<span>c"+iter+"</span>").appendTo(oc.addClass("testdebug").css("position","absolute").css("padding","0px")
																												.css("border","1px solid black").css("margin","-1").appendTo("body"));
					}

					// Calculate protrusions likewise:
					oc     = $($c[ci]).collision( $r, { mode: "protrusion", as: "<div />", colliderData: d1, obstacleData: d2, directionData: d3, relative: "body" } );
					if( DEBUG ) { console.log( "protrusions", oc ); }

					// Add the interaction settings to their data, so we can pick it apart later:
					oc.data( di, ilist[i] );

					// And add the protrusions to the interaction:
					ilist[i].protrusions = ilist[i].protrusions.add( oc );

					// And if there are any, make the appropriate Collision() objects and add them to the list
					if( oc.length > 0 )
					{
						pocl = oc.toArray().map( function(e,i,a){ return new Collision($(e), d1, d2, "protrusion", dx, dy, d3, cog, event.pageX, event.pageY ); } );
						ocl = ocl.concat( pocl );
						if(VISUAL_DEBUG) $("<span>p"+iter+"</span>").appendTo(oc.addClass("testdebug").css("position","absolute").css("padding","0px")
																												.css("border","1px solid magenta").css("margin","-1").appendTo("body"));
					}
				}

				// Now remove coordinate offsets before sending events, otherwise event results might futz with em:
				$c.each( function(){ $(this).removeData( "jquery-collision-coordinates" ); } );
			}

			if( widget.containment )
			{
				// Add offset to coordinates before figuring out collisions, because we're basing it on "where its about to go", not "where it is":
				// (Don't do this for anything but colliders! Applying to obstacles or restrictions or containment screws things up!!)
				$(this).each( function(){ $(this).data( "jquery-collision-coordinates", jq2Coords($(this),dx,dy) ); } );

				// Check protrusion from container as well
				// NOTE: since draggable plugin has already applied containment, if we accidentally move it outside, it won't fix it for us
				var $cc = $(this).collision( $cont, { mode: "protrusion", as: "<div />", colliderData: d1, obstacleData: d2, directionData: d3, relative: "body" } );
						ccl = $cc.toArray().map( function(e,i,a){ return new Collision($(e), d1, d2, "protrusion", dx, dy, d3, jqList2CenterGravity($c), event.pageX, event.pageY ); } );
				if(VISUAL_DEBUG) $("<span>x"+iter+"</span>").appendTo($cc.addClass("testdebug").css("position","absolute")
																											.css("padding","0px").css("border","1px solid blue").css("margin","-1").appendTo("body"));

				// Now remove coordinate offsets before sending events, otherwise event results might futz with em:
				$(this).each( function(){ $(this).removeData( "jquery-collision-coordinates" ); } );
			}

			if( DEBUG ) console.log("checking if we have any collisions at all...");
			// If there's no collisions, INCLUDING the container, stop now, don't keep doing stuff
			if( ( cocl.length <= 0 ) && ( pocl.length <= 0 ) && ( ccl.length <= 0 ) ) break;

			var doneAdjusting = true;

			// Go through each interaction -- if any of them break the prevention rule, we aren't done adjusting yet
			for( var i=0; i<ilist.length; i++ )
			{
				if( DEBUG ) console.log("checking adjustments for",ilist[i],"ccl=",ccl,
																"pc,cl,pp,pl=",ilist[i].preventCollision, ilist[i].collisions.length, ilist[i].preventProtrusion, ilist[i].protrusions.length);

				if( DEBUG ) console.log("checking if we overstepped our containment...");
				// If we aren't trying to prevent anything yet we SOMEHOW jumped our containment, stop - this shouldn't ever happen, DANGER, WILL ROBINSON!
				if( ( ! ilist[i].preventCollision ) && ( ! ilist[i].preventProtrusion ) && ( ccl.length > 0 ) )
				{
					if( DEBUG ) { console.log( "not trying to prevent anything, but jumped our containment", ilist[i] ); }
					doneAdjusting = false;
				}

				if( DEBUG ) console.log("checking if we want to block something we have collided with...");
				// More specifically, if aren't any collisions that we actually want to prevent, stop -- though we have to think of this in the opposite sense:
				// if we DO either
				//   want to prevent collisions yet have a collision or containment failure, OR
				//   want to prevent protrusions yet have a protrusion or a containment failure,
				// then DON'T STOP
				if( ( ilist[i].preventCollision  && ( ( ilist[i].collisions .length > 0 ) || ( ccl.length > 0 ) ) ) ||
						( ilist[i].preventProtrusion && ( ( ilist[i].protrusions.length > 0 ) || ( ccl.length > 0 ) ) )    )
				{
					if( DEBUG ) { console.log( "trying to prevent something that we're still hitting", ilist[i] ); }
					doneAdjusting = false;
				}
			}

			if( doneAdjusting )
			{
				if( DEBUG ) { console.log( "done adjusting" ); }
				break;
			}

			if( DEBUG ) console.log("calculating delta with ocl,ccl=",ocl,ccl);
			// Calculate a delta to move, based on collisions+protrusions and containment
			var d = delta( ocl.concat(), ccl, deltaCache );

			if( DEBUG ) console.log("dx=",d.dx,"dy=",d.dy);
			// If there's nothing to do, stop -- it shouldn't happen if we had collisions, but...
			if( d.dx == 0 && d.dy == 0 ) break;

			// Apply the movement, and let the loop run again, to see if our proposed delta movement was any good
			ui.position.left += d.dx;
			ui.position.top  += d.dy;
		}

		dx = ui.position.left - rp.left;
		dy = ui.position.top  - rp.top;
		/* deactivated for now - doesn't seem to be needed - may revisit later:
			// if our new center of gravity is further from the mouse position than the last one, revert
			var origd = jqList2CenterGravity($c,origdx,origdy).distanceTo( event.pageX, event.pageY );
			var  newd = jqList2CenterGravity($c,    dx,    dy).distanceTo( event.pageX, event.pageY );
			if( newd > origd ) { console.log("center of gravity issue: ",origd,newd); } // add this to revert
		*/

		// If we failed to find a fit, revert to the previous position
		if( ( iter > maxiter ) ||                             // if we ran out of iterations, tough, revert
				( ccl.length > 0 ) ||                             // if we ran outside out containment, also revert
				( o.preventProtrusion && ( pocl.length > 0 ) ) || // if we have a protrusion and are trying to prevent protrusions, revert
				( o.preventCollision  && ( cocl.length > 0 ) ) )  // if we have a collision and are trying to prevent collisions, revert
		{
			if( DEBUG ) console.log("reverting, i=",iter,"maxiter=",maxiter,"cocl=",cocl,"cocl.len=",cocl.length,"pocl=",
															 pocl,"pocl.len=",pocl.length,"ccl=",ccl,"ccl.len=",ccl.length,
															 //"newd=",newd,"origd=",origd,
															 "origdx=",origdx,"origdy=",origdy,"dx=",dx,"dy=",dy);
			ui.position.left = origleft;
			ui.position.top  = origtop;
		}

		// NOW we can go through and actually send out the events -- we couldn't before, because we might have hit
		//   collisions multiple times during the course of trying to prevent them
		for( var ci=0; ci<ocl.length; ci++ )
		{
			var oc = ocl[ci]; // each ocl[n] is a Collision()
			for( var oci=0; oci<oc.collision.length; oci++ )
			{
				var $occ    = $( oc.collision[oci] );

				// Remove our custom data elements that guaranteed us getting the data we needed
				var $coll   = $( $occ.data(d1) );
				var $obs    = $( $occ.data(d2) );
				var dir     =    $occ.data(d3);
				var inter   =    $occ.data(di);
				$occ.removeData(d1).removeData(d2).removeData(d3).removeData(di);

				// And add them back in if the user really wanted them after all
				if(        inter.colliderData  ) $occ.data( inter.colliderData,  $coll ); // not that useful, since event gets it, but meh
				if(        inter.obstacleData  ) $occ.data( inter.obstacleData,  $obs  ); // not that useful, since event gets it, but meh
				if( dir && inter.directionData ) $occ.data( inter.directionData, dir   );

				if( inter.relative && inter.relative != "body" )
				{
					var off = $occ.offset();
					var rel = inter.relative == "collider" ? $coll :
										inter.relative == "obstacle" ? $obs  :
																									 $(inter.relative);
					var ro  = rel.offset();
					$occ.offset( { left: off.left-ro.left, top: off.top-ro.top } );
				}

				// Send actual collision event - one per collision, i.e. per collider per collided:
				if( inter.obstacleSelector  && ( oc.type == "collision"  ) )
				{
					e = new CollisionEvent( ctyp, $coll, $obs, ctyp, $occ );
					uiTrigger( $coll, widget, ctyp, e, ui );
				}
				if( inter.restraintSelector && ( oc.type == "protrusion" ) )
				{
					e = new CollisionEvent( ptyp, $coll, $obs, ptyp, $occ );
					uiTrigger( $coll, widget, ptyp, e, ui );
				}
			}
		}

		// Global just-checked-collisions event:
		for( var i=0; i<ilist.length; i++ )
		{
			if( ilist[i].obstacleSelector )
			{
				e = new CollisionCheckEvent( postc, $(this), ilist[i].obstacle, ctyp )
				uiTrigger( ilist[i].collider, widget, postc, e, ui );
			}
			if( ilist[i].restraintSelector )
			{
				e = new CollisionCheckEvent( postp, $(this), ilist[i].restraint, ptyp )
				uiTrigger( ilist[i].collider, widget, postp, e, ui );
			}
		}

		// And put the resulting ui position in our cache, so that if we keep dragging, we'll know how far stuff moved since this time
		$(this).data( "jquery-ui-draggable-collision-recent-position", ui.position );
	}

	// This is the inner-loop collision-prevention function, called maxiter times inside handleCollide()
	// Its purpose is to determine a single [dx,dy] to move the whole list of colliders and draggable,
	//   in an attempt to fit them properly. Only one of dx or dy will be non-zero at a time.
	// The cache argument is a simple object passed in from handleCollide, and is used to store previously-
	//   -tried movements, so that it doesn't repeat itself.
	//   * The most common repeat case is if a collider is stuck inbetween two obstacles, and the space isn't big
	//     enough -- one iteration will have it clear obstacle A, but embed into obstacle B, and the next iteration
	//     will reverse it, and it will get nowhere quickly.
	//   * The key for the hash is collider+obstacle coordinates, so the same collider won't avoid the same obstacle
	//     in the same way twice. (Actually three times, see below.)
	//   * Note that the value of the hash is either nothing, "tried normal", or "tried reverse"
	//     * Nothing means it hasn't been tried yet
	//     * Tried normal means it was tried once
	//     * Tried reverse means it was tried a second time
	// How does it determine how to move things around?
	//   * It calculates an "embed" value, i.e. how far the collider is "embedded" into the obstacle
	//   * And a direction, either towards center of gravity or away, depending on if it's a collision or protrusion
	//   * And it returns that
	// How does it choose what obstacle to avoid?
	//   * It first sorts all the "collisions" (remember, these are the actual overlapping regions) based on how
	//     far their centers of gravity are from the last known center of gravity of the draggable itself,
	//     which was passed in during the creation of the Collision objects
	//   * It looks at the last one first, so the furthest from the object -- that way, it's going to always reign
	//     it in towards the last known good value, otherwise "closer" obstacles might keep shoving it away forever
	//   * Next, it calculates the embed values in the x and y direction, and picks the smallest one -- so if it's
	//     embedded heavily in the x-direction and only a little in the y, it'll pick the y first (small changes are
	//     more likely to succeed)
	//   * If it's a negative value, it tries again, because the embed function does the sanity-checking if we
	//     tried to move it in a strange or useless way
	//   * Finally, this is why it tries each "position" twice -- because for each starting position, it tries the
	//     smaller movement first, but then it tries the larger one. That's why the cache keeps track of the
	//     "tried reverse" value
	// In the end, it returns a { dx:_, dy:_ } object, the handleCollision tries moving everything around, recalculates
	//   all the collisions, and calls this again to see what to do next
	// NOTE: you can't easily re-calc "real position" from the jquery objects, because they're tagged with data that has
	//       offsets that are used internally by the jquery-collision plugin, so only use the thisc.*Coords values
	function delta( collisions, containments, cache )
	{
		var c = collisions.concat(containments).sort( collisionComparison );
		if( VISUAL_DEBUG ) { if(!cache.deltanum){ cache.deltanum = 1; } }
		if( DEBUG ) { console.log( "collisions, in order: ", c.map(function(e,i,a){return e.collisionCoords.hash();}).join(",") ); }
		while( c.length > 0 )
		{
			// note _pop_ not _shift_. we want to grab furthest collision from center of mass first...
			// this one is likely the one causing the most problems, because something is embedded deeply into
			// some obstacle:
			var thisc = c.pop();
			var co = thisc.obstacleCoords;
			var cc = thisc.colliderCoords;
			var cv = thisc.collisionCoords;
			var ct = thisc.type;
			var cd = thisc.direction;
			var key = thisc.hash();
			var dirx = ( thisc.type=="protrusion" ? ( cc.centerx() > co.centerx() ? -1 : +1 )
																						: ( cc.centerx() > co.centerx() ? +1 : -1 ) );
			var diry = ( thisc.type=="protrusion" ? ( cc.centery() > co.centery() ? -1 : +1 )
																						: ( cc.centery() > co.centery() ? +1 : -1 ) );
			var dx = thisc.embedx( dirx );
			var dy = thisc.embedy( diry );
			if( DEBUG ) console.log("cv,cc,co,ct,dx,dy,thisc.dx,thisc.dy,dirx,diry,co.centerx,co.centery,cc.centerx,cc.centery,key=",
									cv.hash(),cc.hash(),co.hash(),ct,dx,dy,thisc.dx,thisc.dy,dirx,diry,co.centerx(),co.centery(),cc.centerx(),cc.centery(),key);
			var tryToAdjustDX = ( dx < dy );
			if(      key in cache && cache[key] == "tried reverse" ) { if( DEBUG ) console.log("but already tried reverse too...");
																																 continue; }
			else if( key in cache && cache[key] == "tried normal"  ) { if( DEBUG ) console.log("but already tried this...");
																																 tryToAdjustDX=!tryToAdjustDX; cache[key]="tried reverse";
																															 }
			else                                                     { cache[key]="tried normal"; }
			if( tryToAdjustDX )
			{
				if( VISUAL_DEBUG ) { $("<span>"+thisc.direction+"d"+cache.deltanum+".dx="+dx+"*"+dirx+"</span>").css("color","black").addClass("testdebug")
														 .css("position","absolute")
														 .css("white-space","nowrap").offset( { left: thisc.mousex, top: thisc.mousey + 20*(cache.deltanum-1) } ).appendTo("body"); cache.deltanum++; };
				if( dx <= 0 ) { c.push(thisc); continue; }
				return { "dx":dx*dirx, "dy":0 };
			}
			else
			{
				if( VISUAL_DEBUG ) { $("<span>"+thisc.direction+"d"+cache.deltanum+".dy="+dy+"*"+diry+"</span>").css("color","black").addClass("testdebug")
														 .css("position","absolute")
														 .css("white-space","nowrap").offset( { left: thisc.mousex, top: thisc.mousey + 20*(cache.deltanum-1) } ).appendTo("body"); cache.deltanum++; };
				if( dy <= 0 ) { c.push(thisc); continue; }
				return { "dx":0, "dy":dy*diry };
			}
		}
		return { "dx":0, "dy":0 };
	};

})(jQuery);