jquery.cytoscape-panzoom.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /* jquery.cytoscape-panzoom.js */
  2. /**
  3. * This file is part of cytoscape.js 2.0.2.
  4. *
  5. * Cytoscape.js is free software: you can redistribute it and/or modify it
  6. * under the terms of the GNU Lesser General Public License as published by the Free
  7. * Software Foundation, either version 3 of the License, or (at your option) any
  8. * later version.
  9. *
  10. * Cytoscape.js is distributed in the hope that it will be useful, but WITHOUT
  11. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Lesser General Public License along with
  16. * cytoscape.js. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. ;(function($){
  19. var defaults = {
  20. zoomFactor: 0.05, // zoom factor per zoom tick
  21. zoomDelay: 45, // how many ms between zoom ticks
  22. minZoom: 0.1, // min zoom level
  23. maxZoom: 10, // max zoom level
  24. fitPadding: 50, // padding when fitting
  25. panSpeed: 10, // how many ms in between pan ticks
  26. panDistance: 10, // max pan distance per tick
  27. panDragAreaSize: 75, // the length of the pan drag box in which the vector for panning is calculated (bigger = finer control of pan speed and direction)
  28. panMinPercentSpeed: 0.25, // the slowest speed we can pan by (as a percent of panSpeed)
  29. panInactiveArea: 8, // radius of inactive area in pan drag box
  30. panIndicatorMinOpacity: 0.5, // min opacity of pan indicator (the draggable nib); scales from this to 1.0
  31. autodisableForMobile: true, // disable the panzoom completely for mobile (since we don't really need it with gestures like pinch to zoom)
  32. sliderHandleIcon: 'icon-minus',
  33. zoomInIcon: 'icon-plus',
  34. zoomOutIcon: 'icon-minus',
  35. resetIcon: 'icon-resize-full'
  36. };
  37. $.fn.cytoscapePanzoom = function(params){
  38. var options = $.extend(true, {}, defaults, params);
  39. var fn = params;
  40. var functions = {
  41. destroy: function(){
  42. var $this = $(this);
  43. $this.find(".ui-cytoscape-panzoom").remove();
  44. },
  45. init: function(){
  46. var browserIsMobile = 'ontouchstart' in window;
  47. if( browserIsMobile && options.autodisableForMobile ){
  48. return $(this);
  49. }
  50. return $(this).each(function(){
  51. var $container = $(this);
  52. var $panzoom = $('<div class="ui-cytoscape-panzoom"></div>');
  53. $container.append( $panzoom );
  54. if( options.staticPosition ){
  55. $panzoom.addClass("ui-cytoscape-panzoom-static");
  56. }
  57. // add base html elements
  58. /////////////////////////
  59. var $zoomIn = $('<div class="ui-cytoscape-panzoom-zoom-in ui-cytoscape-panzoom-zoom-button"><span class="icon '+ options.zoomInIcon +'"></span></div>');
  60. $panzoom.append( $zoomIn );
  61. var $zoomOut = $('<div class="ui-cytoscape-panzoom-zoom-out ui-cytoscape-panzoom-zoom-button"><span class="icon ' + options.zoomOutIcon + '"></span></div>');
  62. $panzoom.append( $zoomOut );
  63. var $reset = $('<div class="ui-cytoscape-panzoom-reset ui-cytoscape-panzoom-zoom-button"><span class="icon ' + options.resetIcon + '"></span></div>');
  64. $panzoom.append( $reset );
  65. var $slider = $('<div class="ui-cytoscape-panzoom-slider"></div>');
  66. $panzoom.append( $slider );
  67. $slider.append('<div class="ui-cytoscape-panzoom-slider-background"></div>');
  68. var $sliderHandle = $('<div class="ui-cytoscape-panzoom-slider-handle"><span class="icon ' + options.sliderHandleIcon + '"></span></div>');
  69. $slider.append( $sliderHandle );
  70. var $noZoomTick = $('<div class="ui-cytoscape-panzoom-no-zoom-tick"></div>');
  71. $slider.append( $noZoomTick );
  72. var $panner = $('<div class="ui-cytoscape-panzoom-panner"></div>');
  73. $panzoom.append( $panner );
  74. var $pHandle = $('<div class="ui-cytoscape-panzoom-panner-handle"></div>');
  75. $panner.append( $pHandle );
  76. var $pUp = $('<div class="ui-cytoscape-panzoom-pan-up ui-cytoscape-panzoom-pan-button"></div>');
  77. var $pDown = $('<div class="ui-cytoscape-panzoom-pan-down ui-cytoscape-panzoom-pan-button"></div>');
  78. var $pLeft = $('<div class="ui-cytoscape-panzoom-pan-left ui-cytoscape-panzoom-pan-button"></div>');
  79. var $pRight = $('<div class="ui-cytoscape-panzoom-pan-right ui-cytoscape-panzoom-pan-button"></div>');
  80. $panner.append( $pUp ).append( $pDown ).append( $pLeft ).append( $pRight );
  81. var $pIndicator = $('<div class="ui-cytoscape-panzoom-pan-indicator"></div>');
  82. $panner.append( $pIndicator );
  83. // functions for calculating panning
  84. ////////////////////////////////////
  85. function handle2pan(e){
  86. var v = {
  87. x: e.originalEvent.pageX - $panner.offset().left - $panner.width()/2,
  88. y: e.originalEvent.pageY - $panner.offset().top - $panner.height()/2
  89. }
  90. var r = options.panDragAreaSize;
  91. var d = Math.sqrt( v.x*v.x + v.y*v.y );
  92. var percent = Math.min( d/r, 1 );
  93. if( d < options.panInactiveArea ){
  94. return {
  95. x: NaN,
  96. y: NaN
  97. };
  98. }
  99. v = {
  100. x: v.x/d,
  101. y: v.y/d
  102. };
  103. percent = Math.max( options.panMinPercentSpeed, percent );
  104. var vnorm = {
  105. x: -1 * v.x * (percent * options.panDistance),
  106. y: -1 * v.y * (percent * options.panDistance)
  107. };
  108. return vnorm;
  109. }
  110. function donePanning(){
  111. clearInterval(panInterval);
  112. $(window).unbind("mousemove", handler);
  113. $pIndicator.hide();
  114. }
  115. function positionIndicator(pan){
  116. var v = pan;
  117. var d = Math.sqrt( v.x*v.x + v.y*v.y );
  118. var vnorm = {
  119. x: -1 * v.x/d,
  120. y: -1 * v.y/d
  121. };
  122. var w = $panner.width();
  123. var h = $panner.height();
  124. var percent = d/options.panDistance;
  125. var opacity = Math.max( options.panIndicatorMinOpacity, percent );
  126. var color = 255 - Math.round( opacity * 255 );
  127. $pIndicator.show().css({
  128. left: w/2 * vnorm.x + w/2,
  129. top: h/2 * vnorm.y + h/2,
  130. background: "rgb(" + color + ", " + color + ", " + color + ")"
  131. });
  132. }
  133. function calculateZoomCenterPoint(){
  134. var cy = $container.cytoscape("get");
  135. var pan = cy.pan();
  136. var zoom = cy.zoom();
  137. zx = $container.width()/2;
  138. zy = $container.height()/2;
  139. }
  140. var zooming = false;
  141. function startZooming(){
  142. zooming = true;
  143. calculateZoomCenterPoint();
  144. }
  145. function endZooming(){
  146. zooming = false;
  147. }
  148. var zx, zy;
  149. function zoomTo(level){
  150. var cy = $container.cytoscape("get");
  151. if( !zooming ){ // for non-continuous zooming (e.g. click slider at pt)
  152. calculateZoomCenterPoint();
  153. }
  154. cy.zoom({
  155. level: level,
  156. position: { x: zx, y: zy }
  157. });
  158. }
  159. var panInterval;
  160. var handler = function(e){
  161. e.stopPropagation(); // don't trigger dragging of panzoom
  162. e.preventDefault(); // don't cause text selection
  163. clearInterval(panInterval);
  164. var pan = handle2pan(e);
  165. if( isNaN(pan.x) || isNaN(pan.y) ){
  166. $pIndicator.hide();
  167. return;
  168. }
  169. positionIndicator(pan);
  170. panInterval = setInterval(function(){
  171. $container.cytoscape("get").panBy(pan);
  172. }, options.panSpeed);
  173. };
  174. $pHandle.bind("mousedown", function(e){
  175. // handle click of icon
  176. handler(e);
  177. // update on mousemove
  178. $(window).bind("mousemove", handler);
  179. });
  180. $pHandle.bind("mouseup", function(){
  181. donePanning();
  182. });
  183. $(window).bind("mouseup blur", function(){
  184. donePanning();
  185. });
  186. // set up slider behaviour
  187. //////////////////////////
  188. $slider.bind('mousedown', function(){
  189. return false; // so we don't pan close to the slider handle
  190. });
  191. var sliderVal;
  192. var sliding = false;
  193. var sliderPadding = 2;
  194. function setSliderFromMouse(evt, handleOffset){
  195. if( handleOffset === undefined ){
  196. handleOffset = 0;
  197. }
  198. var padding = sliderPadding;
  199. var min = 0 + padding;
  200. var max = $slider.height() - $sliderHandle.height() - 2*padding;
  201. var top = evt.pageY - $slider.offset().top - handleOffset;
  202. // constrain to slider bounds
  203. if( top < min ){ top = min }
  204. if( top > max ){ top = max }
  205. var percent = 1 - (top - min) / ( max - min );
  206. // move the handle
  207. $sliderHandle.css('top', top);
  208. var zmin = options.minZoom;
  209. var zmax = options.maxZoom;
  210. // assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
  211. var x = Math.log(zmin) / Math.log(zmax);
  212. var p = (1 - x)*percent + x;
  213. // change the zoom level
  214. var z = Math.pow( zmax, p );
  215. // bound the zoom value in case of floating pt rounding error
  216. if( z < zmin ){
  217. z = zmin;
  218. } else if( z > zmax ){
  219. z = zmax;
  220. }
  221. zoomTo( z );
  222. }
  223. var sliderMdownHandler, sliderMmoveHandler;
  224. $sliderHandle.bind('mousedown', sliderMdownHandler = function( mdEvt ){
  225. var handleOffset = mdEvt.target === $sliderHandle[0] ? mdEvt.offsetY : 0;
  226. sliding = true;
  227. startZooming();
  228. $sliderHandle.addClass("active");
  229. var lastMove = 0;
  230. $(window).bind('mousemove', sliderMmoveHandler = function( mmEvt ){
  231. var now = +new Date;
  232. // throttle the zooms every 10 ms so we don't call zoom too often and cause lag
  233. if( now > lastMove + 10 ){
  234. lastMove = now;
  235. } else {
  236. return false;
  237. }
  238. setSliderFromMouse(mmEvt, handleOffset);
  239. return false;
  240. });
  241. // unbind when
  242. $(window).bind('mouseup', function(){
  243. $(window).unbind('mousemove', sliderMmoveHandler);
  244. sliding = false;
  245. $sliderHandle.removeClass("active");
  246. endZooming();
  247. });
  248. return false;
  249. });
  250. $slider.bind('mousedown', function(e){
  251. if( e.target !== $sliderHandle[0] ){
  252. sliderMdownHandler(e);
  253. setSliderFromMouse(e);
  254. }
  255. });
  256. function positionSliderFromZoom(){
  257. var cy = $container.cytoscape("get");
  258. var z = cy.zoom();
  259. var zmin = options.minZoom;
  260. var zmax = options.maxZoom;
  261. // assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
  262. var x = Math.log(zmin) / Math.log(zmax);
  263. var p = Math.log(z) / Math.log(zmax);
  264. var percent = 1 - (p - x) / (1 - x); // the 1- bit at the front b/c up is in the -ve y direction
  265. var min = sliderPadding;
  266. var max = $slider.height() - $sliderHandle.height() - 2*sliderPadding;
  267. var top = percent * ( max - min );
  268. // constrain to slider bounds
  269. if( top < min ){ top = min }
  270. if( top > max ){ top = max }
  271. // move the handle
  272. $sliderHandle.css('top', top);
  273. }
  274. positionSliderFromZoom();
  275. var cy = $container.cytoscape("get");
  276. cy.on('zoom', function(){
  277. if( !sliding ){
  278. positionSliderFromZoom();
  279. }
  280. });
  281. // set the position of the zoom=1 tick
  282. (function(){
  283. var z = 1;
  284. var zmin = options.minZoom;
  285. var zmax = options.maxZoom;
  286. // assume (zoom = zmax ^ p) where p ranges on (x, 1) with x negative
  287. var x = Math.log(zmin) / Math.log(zmax);
  288. var p = Math.log(z) / Math.log(zmax);
  289. var percent = 1 - (p - x) / (1 - x); // the 1- bit at the front b/c up is in the -ve y direction
  290. if( percent > 1 || percent < 0 ){
  291. $noZoomTick.hide();
  292. return;
  293. }
  294. var min = sliderPadding;
  295. var max = $slider.height() - $sliderHandle.height() - 2*sliderPadding;
  296. var top = percent * ( max - min );
  297. // constrain to slider bounds
  298. if( top < min ){ top = min }
  299. if( top > max ){ top = max }
  300. $noZoomTick.css('top', top);
  301. })();
  302. // set up zoom in/out buttons
  303. /////////////////////////////
  304. function bindButton($button, factor){
  305. var zoomInterval;
  306. $button.bind("mousedown", function(e){
  307. e.preventDefault();
  308. e.stopPropagation();
  309. if( e.button != 0 ){
  310. return;
  311. }
  312. var cy = $container.cytoscape("get");
  313. startZooming();
  314. zoomInterval = setInterval(function(){
  315. var zoom = cy.zoom();
  316. var lvl = cy.zoom() * factor;
  317. if( lvl < options.minZoom ){
  318. lvl = options.minZoom;
  319. }
  320. if( lvl > options.maxZoom ){
  321. lvl = options.maxZoom;
  322. }
  323. if( (lvl == options.maxZoom && zoom == options.maxZoom) ||
  324. (lvl == options.minZoom && zoom == options.minZoom)
  325. ){
  326. return;
  327. }
  328. zoomTo(lvl);
  329. }, options.zoomDelay);
  330. return false;
  331. });
  332. $(window).bind("mouseup blur", function(){
  333. clearInterval(zoomInterval);
  334. endZooming();
  335. });
  336. }
  337. bindButton( $zoomIn, (1 + options.zoomFactor) );
  338. bindButton( $zoomOut, (1 - options.zoomFactor) );
  339. $reset.bind("mousedown", function(e){
  340. if( e.button != 0 ){
  341. return;
  342. }
  343. var cy = $container.cytoscape("get");
  344. if( cy.elements().size() === 0 ){
  345. cy.reset();
  346. } else {
  347. cy.fit( options.fitPadding );
  348. }
  349. return false;
  350. });
  351. });
  352. }
  353. };
  354. if( functions[fn] ){
  355. return functions[fn].apply(this, Array.prototype.slice.call( arguments, 1 ));
  356. } else if( typeof fn == 'object' || !fn ) {
  357. return functions.init.apply( this, arguments );
  358. } else {
  359. $.error("No such function `"+ fn +"` for jquery.cytoscapePanzoom");
  360. }
  361. return $(this);
  362. };
  363. $.fn.cyPanzoom = $.fn.cytoscapePanzoom;
  364. })(jQuery);