1 // ========================================================================== 2 // Papercube.CanvasController 3 // 4 // License: PaperCube is open source software released under 5 // the MIT License (see license.js) 6 // ========================================================================== 7 8 require('core'); 9 10 /** @class 11 12 This controls the canvas, the canvas is a viewport that allows views to 13 be resolution independent. This is done by keeping track of various zooming 14 parameters and communicating this to the zoomView (preview pane) and the core 15 view that is being used. 16 17 @extends SC.Object 18 @author Peter Bergstrom 19 @version 1.0 20 @copyright 2008-2009 Peter Bergström. 21 @static 22 */ 23 Papercube.canvasController = NodeGraph.NodeGraphDelegate.create( 24 /** @scope Papercube.canvasController.prototype */ { 25 26 /** 27 boolean signifying that the app should be showing the canvas. 28 29 @property {Boolean} 30 @default NO 31 */ 32 shouldShowCanvas: NO, 33 34 /** 35 Fan menu div container. 36 37 @property {DOM Element} 38 */ 39 fanDiv: null, 40 41 /** 42 Fan menu SVG root. 43 44 @property {DOM Element} 45 */ 46 fanSVGRoot: null, 47 48 /** 49 Fan menu SVG. 50 51 @property {DOM Element} 52 */ 53 fanSVG: null, 54 55 /** 56 Fan menu properties. 57 58 @property {Object} 59 */ 60 fans: {}, 61 62 /** 63 Set to YES when the left view is visible. 64 65 @property {Boolean} 66 @default NO 67 */ 68 leftViewShowing: NO, 69 70 /** 71 The scroll zoom cool off. Record scroll time to compare 72 with the _scrollCoolDownAmount. 73 74 @property {Integer} 75 */ 76 _scrollCoolDown: 0, 77 78 /** 79 The amount to wait between scroll zoom events. 80 81 @property {Integer} 82 @default 200 milliseconds 83 */ 84 _scrollCoolDownAmount: 200, 85 86 /** 87 Zoom in or out to center around where the pointer location. 88 89 @param xPos {Integer} The x position of the mouse pointer. 90 @param yPos {Integer} The y position of the mouse pointer. 91 @param zoomIn {boolean} If YES, zoom in, otherwise, zoom out. 92 93 @returns {Boolean} Returns NO if there is an error. 94 */ 95 setZoomToPointerLocation: function(xPos,yPos, zoomIn) 96 { 97 // Calculate the direction (-/+) the zoom step. 98 var zoomStep = zoomIn ? this.zoomStep : -this.zoomStep; 99 100 // Calculate the desired zoomValue. 101 var zoomValue = this.zoomValue+zoomStep; 102 103 // If the zoomValue is smaller or larger then the limits or the left view is visible, bail. 104 if(zoomValue < this.zoomValueMin || zoomValue > this.zoomValueMax || this.leftViewShowing) 105 { 106 return NO; 107 } 108 109 this.set("zoomValue", zoomValue); 110 111 // Calculate the x and y offsets and set it back to the zoomView. 112 var x = xPos-this.canvasLeft+30*zoomValue; 113 var y = yPos-30-this.canvasTop; 114 var pctX = 1- (this.canvasWidth-x)/this.canvasWidth; 115 var pctY = 1- (this.canvasHeight-y)/this.canvasHeight; 116 SC.page.canvas.zoomView.setParams(pctX, pctY); 117 }, 118 119 /** 120 Zoom to a set of coordinates. 121 122 @param xDiff {Integer} The x position of the mouse pointer. 123 @param yDiff {Integer} The y position of the mouse pointer. 124 @param updateZoomPreview {Boolean} If YES, update the zoom preview pane. 125 126 @returns {Boolean} Returns NO if there is an error. 127 */ 128 panToCoordinates: function(xDiff, yDiff, updateZoomPreview) 129 { 130 131 var zoomValue = this.get('zoomValue'); 132 133 // If the zoomValue is equal to the zoomValueMin, bail. 134 if(zoomValue === this.zoomValueMin || this.leftViewShowing) 135 { 136 return NO; 137 } 138 139 // Calculate the x and y offsets and set it back to the zoomView. 140 var xPos = this.percentageX*this.canvasWidth-xDiff; 141 var yPos = this.percentageY*this.canvasHeight-yDiff; 142 143 var pctX = Math.max(1- (this.canvasWidth-xPos)/this.canvasWidth,0); 144 var pctY = Math.max(1- (this.canvasHeight-yPos)/this.canvasHeight,0); 145 146 this.set("percentageX", pctX); 147 this.set("percentageY", pctY); 148 if(updateZoomPreview) 149 { 150 SC.page.canvas.zoomView._zoomDirty = YES; 151 SC.page.canvas.zoomView.updatePreviewBox(pctX, pctY); 152 } 153 154 }, 155 156 /** 157 Zoom in or out to center around a desired x and y percentage. 158 159 @param pctX {Integer} The x position percentage. 160 @param pctY {Integer} The y position percentage. 161 @param zoomValue {Integer} The desired zoomValue. 162 163 @returns {Boolean} Returns NO if there is an error. 164 */ 165 zoomToLocation: function(pctX, pctY, zoomValue) 166 { 167 // If the zoomValue is smaller or larger then the limits or the left view is visible, bail. 168 if(zoomValue < this.zoomValueMin || zoomValue > this.zoomValueMax || this.leftViewShowing) 169 { 170 return NO; 171 } 172 173 SC.page.canvas.zoomView._zoomDirty = YES; 174 175 this.beginPropertyChanges(); 176 this.set("zoomValue", zoomValue); 177 this.set("percentageX", pctX/2); 178 this.set("percentageY", pctY/2); 179 this.endPropertyChanges(); 180 181 // Set the percentages and the zoom value. 182 // SC.page.canvas.zoomView.updatePreviewBox(pctX, pctY); 183 184 }, 185 186 /** 187 Get window properties and set the portal dimensions then reposition the canvas. 188 */ 189 getWindowProperties: function() 190 { 191 var wH = NodeGraph.windowHeight(); 192 var wW = NodeGraph.windowWidth(); 193 194 this.fanSVGRoot.setAttributeNS(null, "height", wH); 195 this.fanSVGRoot.setAttributeNS(null, "width", wW); 196 197 this.set("portalHeight", (wH-30)); 198 this.set("portalWidth", wW); 199 this.repositionCanvasDidChange(); 200 }, 201 202 /** 203 Reposition the canvas inside the portal if needed, This is triggered if the percentages or the zoom value changes. 204 205 @observes percentageX 206 @observes percentageY 207 @observes zoomValue 208 */ 209 repositionCanvasDidChange: function() 210 { 211 sc_super(); 212 213 this.hideFan(); 214 215 }.observes('percentageX', 'percentageY', 'zoomValue'), 216 217 /** 218 Show a fan menu. 219 220 @param x {Integer} The x position. 221 @param y {Integer} The y position. 222 @param name {string} The view's name. 223 @param fanType {string} The type of fan. 224 225 @returns {Boolean} Returns the NO if the fan name is not registered. 226 */ 227 showFan: function(x,y, name, fanType) 228 { 229 // Hide if there are any fans visible. 230 if(this.fanSVG.childNodes.length != 0) 231 { 232 this.hideFan(); 233 } 234 235 // If there is no name registered, then bail. 236 if(!this.fans[name]) 237 { 238 console.log("Fan Error: You need to register your fan views!"); 239 return NO; 240 } 241 242 // If there is no fan for this name and fan type, then create it lazily. 243 else if(!this.fans[name].svg[fanType]) 244 { 245 console.log("Fan Notice: Fan "+name+"-"+fanType+" has not been created, creating now."); 246 this.createFan(name, fanType); 247 } 248 249 // Append to the DOM. 250 this.fanSVG.appendChild(this.fans[name].svg[fanType]); 251 this.fanSVG.setAttributeNS(null, 'x', x-37); 252 this.fanSVG.setAttributeNS(null, 'y', y-157); 253 this.fanSVGRoot.style.cssText = "position: relative; z-index: 1001;"; 254 }, 255 256 /** 257 Hide the currently visible fan. 258 */ 259 hideFan: function() 260 { 261 // If there is a fan visible, remove it from the DOM. 262 if(this.fanSVG && this.fanSVGRoot && this.fanSVG.firstChild) 263 { 264 this.fanSVG.removeChild(this.fanSVG.firstChild); 265 this.fanSVGRoot.style.cssText = ""; 266 } 267 }, 268 269 /** 270 SVG mouse over. 271 272 If the event contains a wedge or fan DOM element, highlight it, otherwise hide the fan. 273 274 @param {DOM Event} evt The mouseOver event. 275 */ 276 svgMouseOver: function(evt) 277 { 278 this._highlightFan(evt, '#555'); 279 }, 280 281 /** 282 SVG mouse over. 283 284 If the event contains a wedge or fan DOM element, remove its highlight, otherwise hide the fan. 285 286 @param {DOM Event} evt The mouseOut event. 287 */ 288 svgMouseOut: function(evt) 289 { 290 this._highlightFan(evt, '#222'); 291 }, 292 293 /** 294 SVG mouse down. 295 296 If the event contains a wedge or fan DOM element, remove its highlight, otherwise hide the fan. 297 298 @param {DOM Event} evt The mouseDown event. 299 */ 300 svgMouseDown: function(evt) 301 { 302 this._highlightFan(evt, '#222'); 303 }, 304 305 /** 306 SVG mouse up. 307 308 Call back to the parent view. 309 310 @param {DOM Event} evt The mouseUp event. 311 */ 312 svgMouseUp: function(evt) 313 { 314 315 // If it is a path, then set the fill to #222. 316 if(evt.target.nodeName == 'path') 317 { 318 evt.target.setAttributeNS(null, 'fill', '#222'); 319 } 320 321 // Hide the fan. 322 this.hideFan(); 323 324 // Excute action. 325 var params = this._getSVGParams(evt); 326 if(params) 327 { 328 this.fans[params.name].opts[params.fanType][params.action](evt); 329 } 330 }, 331 332 /** 333 Mouse up event on the center circle of the fan. Then, just hide the fan. 334 335 @param {DOM Event} evt The mouseUp event. 336 */ 337 circleMouseUp: function(evt) 338 { 339 this.hideFan(); 340 }, 341 342 /** 343 Registed the fans for a view. 344 345 @param name {string} The view's name. 346 @param fans {string} The fan's options. 347 */ 348 registerFans: function(name, fans) 349 { 350 this.fans[name] = { 351 name: name, 352 opts: fans, 353 svg: {}, 354 slices: {} 355 }; 356 }, 357 358 /** 359 For NodeGraphs, register using this generic function. 360 361 @param view {NodeGraph.NodeGraphView} 362 */ 363 fanForNodeGraph: function(view) { 364 365 var refocusFunc = function(evt) 366 { 367 Papercube.viewController.setContentToViewFromGUID(view._mouseDownGUID, view.contentTypeViewing, NO); 368 }.bind(view); 369 370 var citeseerFunc = function(evt) 371 { 372 window.open(Papercube.Paper.find(view._mouseDownGUID).get('url')); 373 }.bind(view); 374 375 var saveFunc = function(evt) 376 { 377 Papercube.viewController.saveObject(view._mouseDownGUID, view.contentTypeViewing); 378 }.bind(view); 379 380 var zoomOutFunc = function(evt) 381 { 382 Papercube.canvasController.setZoomToPointerLocation(Event.pointerX(evt), Event.pointerY(evt), NO); 383 }.bind(view); 384 385 var zoomInFunc = function(evt) 386 { 387 Papercube.canvasController.setZoomToPointerLocation(Event.pointerX(evt), Event.pointerY(evt), YES); 388 }.bind(view); 389 390 var pinLinesFunc = function(evt) 391 { 392 view.pinLinesForObj(); 393 }.bind(view); 394 395 if(view.get('showEdges')) { 396 Papercube.canvasController.registerFans(view.viewName, 397 { 398 focusedFan: { 399 CiteSeer: citeseerFunc, 400 Save: saveFunc, 401 "Pin Lines": pinLinesFunc, 402 "Zoom +": zoomInFunc, 403 "Zoom -": zoomOutFunc 404 }, 405 unfocusedFan: { 406 CiteSeer: citeseerFunc, 407 Save: saveFunc, 408 "Pin Lines": pinLinesFunc, 409 Refocus: refocusFunc, 410 "Zoom +": zoomInFunc, 411 "Zoom -": zoomOutFunc 412 } 413 }); 414 } else { 415 Papercube.canvasController.registerFans(view.viewName, 416 { 417 focusedFan: { 418 CiteSeer: citeseerFunc, 419 Save: saveFunc, 420 "Zoom +": zoomInFunc, 421 "Zoom -": zoomOutFunc 422 }, 423 unfocusedFan: { 424 CiteSeer: citeseerFunc, 425 Save: saveFunc, 426 Refocus: refocusFunc, 427 "Zoom +": zoomInFunc, 428 "Zoom -": zoomOutFunc 429 } 430 }); 431 } 432 }, 433 434 /** 435 Based on the name and the fan type, look for the registered fan and create it. 436 437 Fans are lazily created only when they are requested. 438 439 @param name {string} The view's name. 440 @param fanType {string} The type of fan. 441 */ 442 createFan: function(name, fanType) 443 { 444 var g = this._createSVGElement('g', {}, {}); 445 446 var opts = this.fans[name].opts[fanType]; 447 448 var slices = []; 449 450 var options = []; 451 for(var key in opts) 452 { 453 options.push(key); 454 } 455 456 var sr = 25; 457 var cx = 11; 458 var cy = 150; 459 var r = 150; 460 461 var len = options.length; 462 var delta = (Math.PI/2)/len; 463 var start = (Math.PI/4); 464 465 // This element is created to be the mouse-exited catcher to hide the fan when you mouse out. 466 this._createSVGElement('circle', {cx: cx+20, cy:cy, r:r*2, opacity: '0.0'}, {mouseover: this.circleMouseUp.bind(this)}, g); 467 468 for(var i=0; i<len; i++) 469 { 470 var end = start + delta; 471 472 var sx1 = cx+20 + sr * Math.sin(start); 473 var sy1 = cy - sr * Math.cos(start); 474 var sx2 = cx+20 + sr * Math.sin(end); 475 var sy2 = cy - sr * Math.cos(end); 476 477 var x1 = cx + (r-sr) * Math.sin(start); 478 var y1 = cy - (r-sr) * Math.cos(start); 479 var x2 = cx + (r-sr) * Math.sin(end); 480 var y2 = cy - (r-sr) * Math.cos(end); 481 482 var pathCmd = "M " + sx2 + "," + sy2 + // Start at circle center 483 " A " + sr + "," + sr + // Draw an arc of radius sr 484 " 0 0 0 " + // Arc details... 485 sx1 + "," + sy1 + // Arc goes to to (x2,y2) 486 " L " + x1 + "," + y1 + // Draw line to (x1,y1) 487 " A " + r + "," + r + // Draw an arc of radius r 488 " 0 0 1 " + // Arc details... 489 x2 + "," + y2 + // Arc goes to to (x2,y2) 490 " Z"; // Close path back to (cx,cy) 491 492 slices.push(this._createSVGElement('path', {d: pathCmd, fill: '#222', stroke: '#555', 'stroke-width': 1, opacity: '0.8', fanType: fanType, name: name, action: options[i], index: i}, { 493 mouseout: this.svgMouseOut.bind(this), mousemove: this.svgMouseOver.bind(this), mousedown: this.svgMouseDown.bind(this), mouseup: this.svgMouseUp.bind(this) 494 }, g)); 495 start=end; 496 } 497 498 var delta = 90/len; 499 var start = -45; 500 for(var i=0; i<len; i++) 501 { 502 var end = start + delta; 503 var text = this._createSVGElement('text', {x: 90, y: cy+5, fill: 'white', 'text-anchor': 'right', 'font-size': 10, transform: "rotate("+(((start+end)/2))+" "+(20)+", 160)", fanType: fanType, name: name, action: options[i], index: i}, { 504 mouseout: this.svgMouseOut.bind(this), mousemove: this.svgMouseOver.bind(this), mousedown: this.svgMouseDown.bind(this), mouseup: this.svgMouseUp.bind(this) }, g); 505 var textNode = document.createTextNode(options[i]); 506 text.appendChild(textNode); 507 start=end; 508 } 509 this._createSVGElement('circle', {cx: cx+20, cy:cy, r:sr, fill: '#000', stroke: "none", 'stroke-width': 1, opacity: '0.3', index: -1}, {mouseup: this.circleMouseUp.bind(this)}, g); 510 511 this.fans[name].svg[fanType] = g; 512 this.fans[name].slices[fanType] = slices; 513 }, 514 515 /** 516 Set a fan's highlight or hide the fan. 517 518 @param {DOM Event} evt The mouse event. 519 @param The {string} desired hex fill color of the fan. 520 */ 521 _highlightFan: function(evt, fill) 522 { 523 var params = this._getSVGParams(evt); 524 if(params) 525 { 526 this.fans[params.name].slices[params.fanType][params.index].setAttributeNS(null, 'fill', fill); 527 } 528 else 529 { 530 this.hideFan(); 531 } 532 }, 533 534 /** 535 Get the fan slice details based on the event object passed in. 536 537 @param {DOM Event} evt The mouse event. 538 539 @returns {Array} {object} Returns the SVG params. 540 */ 541 _getSVGParams: function(evt) 542 { 543 var target = evt.target; 544 var index = target.getAttribute('index'); 545 var name = target.getAttribute('name'); 546 var fanType = target.getAttribute('fanType'); 547 var action = target.getAttribute('action'); 548 if(index && name && fanType && action) 549 { 550 return {index: index, name: name, fanType: fanType, action: action}; 551 } 552 return NO; 553 }, 554 555 /** 556 Create a new SVG element. 557 558 @param type {string} The node type. 559 @param attributes {Object} The attributes hash. 560 @param events {Object} The events hash. 561 @param {DOM Element} The parentNode to attach to. 562 563 @returns {Array} {DOM Element} The SVG element that was created. 564 */ 565 _createSVGElement: function(type, attributes, events, parentNode) 566 { 567 var elm = document.createElementNS("http://www.w3.org/2000/svg", type); 568 if(attributes) 569 { 570 for(var key in attributes) 571 { 572 elm.setAttributeNS(null, key, attributes[key]); 573 } 574 } 575 if(events) 576 { 577 for(var key in events) 578 { 579 elm.addEventListener(key, events[key], NO); 580 } 581 } 582 if(parentNode) 583 parentNode.appendChild(elm); 584 585 return elm; 586 }, 587 588 /** 589 Initalization function. Create the SVG elements for the fan. 590 */ 591 init: function() 592 { 593 // Set up a resize observer on the window. 594 Event.observe(window, 'resize', this.getWindowProperties.bind(this)); 595 596 // Set up te scroll observer. 597 if(SC.isFireFox()) 598 { 599 Element.observe(document.body, 'DOMMouseScroll', this.scrollZoom.bind(this)); 600 } 601 else 602 { 603 Element.observe(document.body, 'mousewheel', this.scrollZoom.bind(this)); 604 } 605 606 // Set up the fan div and svg elements. 607 this.fanDiv = $('fan-container'); 608 609 this.fanSVGRoot = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); 610 this.fanSVGRoot.setAttributeNS(null, "height", 1); 611 this.fanSVGRoot.setAttributeNS(null, "width", 1); 612 this.fanDiv.appendChild(this.fanSVGRoot); 613 614 this.fanSVG = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); 615 this.fanSVG.setAttributeNS(null, "height", 300); 616 this.fanSVG.setAttributeNS(null, "width", 300); 617 this.fanSVGRoot.appendChild(this.fanSVG); 618 619 // Set up the basic window properties. 620 this.getWindowProperties(); 621 sc_super(); 622 } 623 624 }) ; 625