1 // ========================================================================== 2 // Papercube.PaperTreeView 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 is a tree map-like view of papers. 13 14 @extends SC.View 15 @extends NodeGraph.DragPanMixin 16 @author Peter Bergstrom 17 @version 1.0 18 @copyright 2008-2009 Peter Bergström. 19 */ 20 Papercube.PaperTreeView = SC.View.extend(NodeGraph.DragPanMixin, 21 /** @scope Papercube.PaperTreeView.prototype */ { 22 23 /** 24 Bind the display properties from the canvasController. 25 26 @property {Array} 27 @binding "Papercube.canvasController.displayProperties" 28 */ 29 displayPropertiesBinding: "Papercube.canvasController.displayProperties", 30 31 /** 32 Bind the view direction from the viewController. 33 34 @property {String} 35 @binding "Papercube.viewController.viewDirection" 36 */ 37 viewDirectionBinding: "Papercube.viewController.viewDirection", 38 39 /** 40 Bind the cite threshold binding. Don't show papers with less cites. 41 42 @property {Integer} 43 @binding "Papercube.paperTreeController.citeThreshold" 44 */ 45 citeThresholdBinding: "Papercube.paperTreeController.citeThreshold", 46 47 /** 48 Cite threshold cached value. 49 50 @property {Integer} 51 @default 1 52 */ 53 _cached_citeThreshold: 1, 54 55 /** 56 Bind the ref threshold binding. Don't show papers with less refs. 57 58 @property {Integer} 59 @binding "Papercube.paperTreeController.refThreshold", 60 */ 61 refThresholdBinding: "Papercube.paperTreeController.refThreshold", 62 63 /** 64 Ref threshold cached value. 65 66 @property {Integer} 67 @default 1 68 */ 69 _cached_refThreshold: 1, 70 71 /** 72 Bind the depth. Don't show items beyond this depth. 73 74 @property {Integer} 75 @binding "Papercube.paperTreeController.depth" 76 */ 77 depthBinding: "Papercube.paperTreeController.depth", 78 79 /** 80 Depth cached value. 81 82 @property {Integer} 83 @default 15 84 */ 85 _cached_depth: 15, 86 87 /** 88 Display references or citations. 89 90 @property {Boolean} 91 @default YES 92 */ 93 _displayRefs: YES, 94 95 /** 96 Cached width of the view's canvas, set by the displayProperties. 97 98 @property {Integer} 99 @default 800px 100 */ 101 _canvasWidth: 800, 102 103 /** 104 Cached height of the view's canvas, set by the displayProperties. Default 600px. 105 106 @property {Integer} 107 */ 108 _canvasHeight: 600, 109 110 /** 111 Cached height property from displayProperties. 112 113 @property {Integer} 114 */ 115 _h: 0, 116 117 /** 118 Cached width property from displayProperties. 119 120 @property {Integer} 121 */ 122 _w: 0, 123 124 /** 125 Cached x-axis offset property from displayProperties. 126 127 @property {Integer} 128 */ 129 _x: 0, 130 131 /** 132 Cached y-axis offset property from displayProperties. 133 134 @property {Integer} 135 */ 136 _y: 0, 137 138 /** 139 The height of a row in the visualization. 140 141 @property {Integer} 142 @default 1px 143 */ 144 _rowHeight: 1, 145 146 /** 147 The deepest explored area. 148 149 @private 150 @property {Integer} 151 @default 1 152 */ 153 _deepestLevel: 1, 154 155 /** 156 Keep track of the things that are already rendered to avoid cycles. 157 158 @private 159 @property {Object} 160 */ 161 _renderTree: {}, 162 163 /** 164 Paper-DOM map 165 166 @private 167 @property {Object} 168 */ 169 _paperDOMMap: {}, 170 171 172 /** 173 The found GUID. 174 175 @property {String} 176 */ 177 _foundGuid: null, 178 179 /** 180 Meta data box small width. 181 182 @property {Integer} 183 @default 400 184 */ 185 _metaDataBoxWidthSmall: 400, 186 187 /** 188 Meta data box small height. 189 190 @property {Integer} 191 @default 120 192 */ 193 _metaDataBoxHeightSmall: 120, 194 195 /** 196 Cached stylesheet reference. 197 198 @property {DOM Element} 199 */ 200 _styleSheet: null, 201 202 /** 203 The deepest explored area. 204 205 @private 206 @property {Integer} 207 @default 0 208 */ 209 _deepest: 0, 210 211 /** 212 The needed guids, call the server to get them. 213 214 @private 215 @property {Object} 216 */ 217 _guidsNeeded: {}, 218 219 /** 220 The node element that is cloned over and over during the construction of the tree. 221 222 @property {DOM Element} 223 */ 224 _node: null, 225 226 /** 227 The text node element that is cloned over and over during the construction of the tree. 228 229 @property {DOM Element} 230 */ 231 _textNode: null, 232 233 /** 234 Outlets for author stat view. 235 236 ["canvas", "metaDataView"] 237 */ 238 outlets: ["canvas", "metaDataView"], 239 240 /** 241 The canvas element, not Canvas Tag, just DIVs. 242 243 @outlet {DOM Element} '.canvas?' 244 */ 245 canvas: ".canvas?", 246 247 /** 248 The DOM element that contains the meta data for an item. Bound to the '.meta-data?' element. 249 250 @outlet {DOM Element} '.meta-data?' 251 */ 252 metaDataView: ".meta-data?", 253 254 /** 255 Hide meta data box. 256 */ 257 _hideMetaData: function() 258 { 259 // Hide meta data view. 260 this.metaDataView.removeClassName("show-meta-data-small"); 261 262 this.removeHighlight(); 263 }, 264 265 /** 266 Show meta data for paper! 267 268 269 @param {DOM Element} evt The mouse event. 270 @param guid {string} The guid of the item. 271 272 @returns {Boolean} Returns NO if there is no content. 273 */ 274 _showMetaData: function(evt, guid) 275 { 276 var x = Event.pointerX(evt)+10; 277 var y = Event.pointerY(evt)-20; 278 279 var wHeight = NodeGraph.windowHeight(); 280 var wWidth = NodeGraph.windowWidth(); 281 282 if(y < 30) y = 30; 283 if(x < 20) x = 20; 284 if(y > (wHeight-this._metaDataBoxHeightSmall)) y = (wHeight-this._metaDataBoxHeightSmall)-30; 285 if(x > (wWidth-this._metaDataBoxWidthSmall)) x = (wWidth-this._metaDataBoxWidthSmall)+1; 286 287 this.metaDataView.style.top = y + "px"; 288 this.metaDataView.style.left = x + "px"; 289 290 291 this.metaDataView.addClassName("show-meta-data-small"); 292 293 if(guid.indexOf('--1') == -1) 294 { 295 // Get the paper content. 296 var content = Papercube.Paper.find(guid); 297 298 if(!content) return; 299 300 // Set the title 301 this.metaDataView.childNodes[0].innerHTML = content.get("title"); 302 303 // Set the authors 304 var authors = content.get('authorNames').join(', '); 305 var authLen = authors.length; 306 this.metaDataView.childNodes[1].innerHTML = (authLen > 150) ? (authors.substr(0,150)+"…") : authors; 307 this.metaDataView.childNodes[2].innerHTML = (content.get('publisher')) ? content.get('publisher') : ''; 308 309 // Set the date 310 this.metaDataView.childNodes[3].innerHTML = "<strong>Publication Date: </strong> " + content.get("year"); 311 312 this.metaDataView.childNodes[4].innerHTML = Papercube.pluralizeString(" reference", content.get('refCount')); 313 this.metaDataView.childNodes[5].innerHTML = Papercube.pluralizeString(" citation", content.get('citeCount')); 314 315 } 316 }, 317 318 // Mouse over, show meta-data. 319 mouseMoved: function(evt) 320 { 321 if(evt && evt.target && evt.target.id && Element.hasClassName(evt.target, 'item') || Element.hasClassName(evt.target, 'item-text')) 322 { 323 var guid = evt.target.id; 324 if(guid.substr(0,2) != '-1') 325 { 326 327 if(guid != this._foundGuid) 328 { 329 this.removeHighlight(); 330 } 331 332 this._showMetaData(evt, guid); 333 this._foundGuid = guid; 334 335 var nodes = this._paperDOMMap[this._foundGuid]; 336 337 if(nodes) 338 { 339 for(var i=0; i<nodes.length; i++) 340 { 341 nodes[i].addClassName('highlight'); 342 } 343 } 344 } 345 else 346 { 347 this._hideMetaData(); 348 } 349 } 350 else 351 { 352 this._hideMetaData(); 353 } 354 }, 355 356 357 /** 358 Remove the highlight of redundant nodes. 359 */ 360 removeHighlight: function() 361 { 362 var nodes = this._paperDOMMap[this._foundGuid]; 363 364 if(!nodes) return; 365 366 for(var i=0; i<nodes.length; i++) 367 { 368 nodes[i].removeClassName('highlight'); 369 } 370 }, 371 372 /** 373 When the mouse is moved out of the view, remove the class name that makes it visible. 374 375 @param {DOM Event} evt The mouseExited event. 376 */ 377 mouseExited: function(evt) 378 { 379 this._hideMetaData(); 380 }, 381 382 /** 383 Save the GUID of the element and then show the fan. 384 385 @param {DOM Event} evt The mouseDown event. 386 */ 387 mouseDown: function(evt) 388 { 389 if(evt && evt.target && evt.target.id) 390 { 391 var guid = evt.target.id; 392 if(guid.substr(0,2) == '--1') 393 { 394 Papercube.viewController.setContentToViewFromGUID(guid.substr(3,guid.length-3), "Paper"); 395 } 396 else 397 { 398 this._mouseDownGUID = guid; 399 400 var type = (Element.hasClassName(evt.target, 'level0') || (Element.hasClassName(evt.target, 'item-text') && Element.hasClassName(evt.target.parentNode, 'level0'))) ? 'focused' : 'unfocused'; 401 402 Papercube.canvasController.showFan(Event.pointerX(evt), Event.pointerY(evt), 'papertree', (type+"Fan")); 403 return YES; 404 } 405 } 406 else 407 { 408 return this.handleMouseDownDrag(evt); 409 } 410 return NO; 411 }, 412 413 /** 414 Redraw based on 'content', 'isVisible', 'viewDirection', 'refThreshold', 'depth', 'citeThreshold' binding changes. 415 416 @observes content 417 @observes isVisible 418 @observes viewDirection 419 @observes refThreshold 420 @observes depth 421 @observes citeThreshold 422 423 @returns {Boolean} Returns NO if there is no guid of if the view is not visible. 424 */ 425 redrawParamsDidChange: function() 426 { 427 428 var content = this.get('content'); 429 430 // If there is no content or if you're not visible, bail. 431 if(!this.get("isVisible") || !content) return NO; 432 433 Papercube.canvasController.set("zoomValueMax", 5); 434 Papercube.canvasController.set("zoomStep", .5); 435 436 437 438 this._hideMetaData(); 439 440 if(!this._cachedContent || this._cachedContent.get('guid') !== content.get('guid')) 441 { 442 // Hide meta data view. 443 this._deepest = 1; 444 Papercube.canvasController.zoomOut(); 445 Papercube.paperTreeController.setDefaults(); 446 } 447 448 // Set the view direction. 449 this._displayRefs = (this.get("viewDirection") == "References"); 450 451 this.setClassName('citations', !this._displayRefs); 452 this.setClassName('references', this._displayRefs); 453 454 // Grab the cite/ref threshold. 455 var citeThreshold = this.get('citeThreshold'); 456 var refThreshold = this.get('refThreshold'); 457 458 this._cached_citeThreshold = citeThreshold; 459 this._cached_refThreshold = refThreshold; 460 this._cached_depth = this.get('depth'); 461 462 this._cachedContent = content; 463 464 this.displayPropertiesDidChange(); 465 466 }.observes('content', 'isVisible', 'viewDirection', 'refThreshold', 'depth', 'citeThreshold'), 467 468 469 /** 470 When the displayProperities binding changes, update the view appropriately. 471 472 @observes displayProperties 473 474 @param force {boolean} If YES, force a redraw. 475 476 @returns {Boolean} Returns NO if there is no guid of if the view is not visible. 477 */ 478 displayPropertiesDidChange: function() 479 { 480 481 if(!this.get("isVisible") || !this._cachedContent) return NO; 482 483 // Save display properties locally. 484 var h = this.displayProperties.height-20; 485 var w = this.displayProperties.width-20; 486 var x = this.displayProperties.left+20; 487 var y = this.displayProperties.top; 488 var z = this.displayProperties.zoomValue; 489 490 // Set the style and frame of the view. This replaces set('frame', {...}) due to performance reasons. 491 this.setStyle({height: h+"px", width: w+'px', left: x+'px', top: y+'px'}); 492 this._frame = {height: h, width: w, x: x, y: y}; 493 494 // Set the canvas dimensions. 495 this.canvas.style.height = h +"px"; 496 this.canvas.style.width = w+"px"; 497 this.canvas.style.left = x+'px'; 498 this.canvas.style.top = y+'px'; 499 500 // Save the canvas height and width. 501 this._canvasHeight = h; 502 this._canvasWidth = w; 503 504 // Save the properties as needed. 505 this._h = h; 506 this._w = w; 507 this._x = x; 508 this._y = y; 509 this._z = z; 510 511 this._render(); 512 513 }.observes('displayProperties'), 514 515 /** 516 Collect what needs to be rendered. 517 518 Then render the view. 519 520 */ 521 _render: function() 522 { 523 this._guidsNeeded = {}; 524 this._renderTree = {}; 525 this._deepest = 0; 526 this._paperDOMMap = {}; 527 528 var node = this._buildLevel(this._cachedContent.get('guid'), 0, this._w, 0, 0); 529 530 // Now modify the class. 531 this._rowHeight = Math.floor(this._h/((Math.min(this._deepest+1, this.get('depth'))))); 532 533 var styleSheet = this._styleSheet; 534 535 if(this._displayRefs) 536 { 537 styleSheet.cssRules[0].style.top = this._rowHeight +"px"; 538 styleSheet.cssRules[0].style.bottom = ''; 539 } 540 else 541 { 542 styleSheet.cssRules[0].style.top = ''; 543 styleSheet.cssRules[0].style.bottom = this._rowHeight +"px"; 544 } 545 styleSheet.cssRules[0].style.height = this._rowHeight +"px"; 546 styleSheet.cssRules[1].style.height = (this._rowHeight-10) +"px"; 547 548 if(this.canvas.childNodes.length) 549 this.canvas.removeChild(this.canvas.childNodes[0]); 550 551 if(node) 552 { 553 this.canvas.appendChild(node); 554 } 555 556 // Collect any guids that we need to retrieve from the server. 557 var guids = []; 558 for(var key in this._guidsNeeded) 559 { 560 if(guids.indexOf(key) == -1) 561 { 562 guids.push(key); 563 } 564 } 565 566 if(guids.length > 0) 567 { 568 Papercube.searchController.set('showRequestSpinner', YES); 569 // Now retrieve the next level as needed.. 570 var callBack = function() { this.redrawParamsDidChange(); }.bind(this, this.get('content')); 571 Papercube.adaptor.getPaperDetails(guids, callBack); 572 this._cachedContent.set((this._displayRefs ? "maxRefLevel" : "maxCiteLevel"), this._deepest); 573 } 574 else 575 { 576 Papercube.searchController.set('showRequestSpinner', NO); 577 } 578 }, 579 580 /** 581 Recursively draw the tree. 582 583 @param guid {string} The guid of the item. 584 @param level {Integer} The current level. 585 @param parentWidth {Integer} The width of the parent element. 586 @param masterLeft {Integer} The left position. 587 @param idx {Integer} The position of the relation. 588 589 @returns {Array} {DOM Element} Returns the contstructed node to be appended to the parent. 590 */ 591 _buildLevel: function(guid, level, parentWidth, masterLeft, idx) 592 { 593 // If we have hit the end of what we want to show, bail bail bail. 594 if(this._cached_depth < level) 595 { 596 return null; 597 } 598 599 // Get the paper. 600 var paper = Papercube.Paper.find(guid); 601 var rels = []; 602 var displayRefs = this._displayRefs; 603 604 // If the paper exists, then get its references or citations. 605 if(paper) 606 { 607 rels = (displayRefs) ? paper._attributes.references : paper._attributes.citations; 608 } 609 610 // Log the deepest level so we know what height to apply to all elements. 611 if(this._deepest < level) this._deepest = level; 612 613 // If the paper has not been printed before and there is at least 1 pixels for each paper, draw it. 614 if(paper) 615 { 616 var refCount = paper.get('refCount'); 617 var citeCount = paper.get('citeCount'); 618 619 var node = this._node.cloneNode(YES); 620 node.className = 'item level'+level; 621 node.id = guid; 622 if(level !== 0) 623 node.style.left = (masterLeft-1)+'px'; 624 625 node.style.width = (parentWidth-1)+'px'; 626 627 if(!this._paperDOMMap[guid]) this._paperDOMMap[guid] = []; 628 629 this._paperDOMMap[guid].push(node); 630 631 // If the parentWidth is at least 20 px, then output some info. 632 if(parentWidth > 20 && paper) 633 { 634 var textNode = this._textNode.cloneNode(YES); 635 textNode.id = guid; 636 textNode.innerHTML = paper._attributes.title + '<br/><br/> ['+ 637 Papercube.pluralizeString(" ref", refCount) + '] [' + 638 Papercube.pluralizeString(" cite", citeCount) + ']'; 639 node.appendChild(textNode); 640 } 641 642 if(!this._renderTree[guid]) this._renderTree[guid] = 0; 643 644 this._renderTree[guid]++; 645 646 var childCount = rels.length; 647 var newRels = []; 648 for(var i=0; i<childCount; i++) 649 { 650 var relGuid = rels[i]; 651 var child = Papercube.Paper.find(relGuid); 652 if(child) 653 { 654 if(this._cached_citeThreshold <= child.get('citeCount') && 655 this._cached_refThreshold <= child.get('refCount')) 656 { 657 newRels.push(relGuid); 658 } 659 } 660 else 661 { 662 this._guidsNeeded[relGuid] = 1; 663 } 664 } 665 666 childCount = newRels.length; 667 rels = newRels; 668 669 // Calculate the parameters needed for the next level. 670 var myWidth = Math.floor(parentWidth/childCount); 671 672 if(myWidth >= 1 && this._renderTree[guid] == 1) 673 { 674 var diff = parentWidth-(myWidth*childCount); 675 var left = 0; 676 var nextLvl = level+1; 677 678 for(var i=0; i<childCount; i++) 679 { 680 var rel = rels[i]; 681 var w = myWidth; 682 if(diff != 0 && i < diff) 683 { 684 w = myWidth+1; 685 } 686 var nodeLevel = this._buildLevel(rel, nextLvl, w, left, i); 687 if(nodeLevel) 688 node.appendChild(nodeLevel); 689 this._renderTree[rel] = 1; 690 left += w; 691 } 692 } 693 // Not sure how to deal with the threshold.. Currently, don't render it just don't show the inspector and a click will refocus to parent. 694 else if(this._renderTree[guid] != 1 && myWidth <= 1) 695 { 696 var node = this._node.cloneNode(YES); 697 node.className = 'item stub level'+level; 698 node.id = '--1_'+guid; 699 node.style.width = (parentWidth-1)+'px'; 700 } 701 } 702 // Not sure how to deal with the threshold.. Currently, don't render it just don't show the inspector and a click will refocus to parent. 703 else if(this._renderTree[guid] != 1 && myWidth <= 1) 704 { 705 var node = this._node.cloneNode(YES); 706 node.className = 'item stub level'+level; 707 node.id = '--1_'+guid; 708 node.style.width = (parentWidth-1)+'px'; 709 } 710 711 return node; 712 }, 713 714 /** 715 Initalization function. 716 717 Set up DOM nodes to be cloned for the tree. 718 719 Grab the stylesheet. 720 721 Set up the fan menu actions: 722 723 focusedFan: { 724 CiteSeer: citeseerFunc, 725 Save: saveFunc, 726 "Zoom +": zoomInFunc, 727 "Zoom -": zoomOutFunc 728 }, 729 unfocusedFan: { 730 CiteSeer: citeseerFunc, 731 Save: saveFunc, 732 Refocus: refocusFunc, 733 "Zoom +": zoomInFunc, 734 "Zoom -": zoomOutFunc 735 } 736 */ 737 init: function() 738 { 739 740 var refocusFunc = function(evt) 741 { 742 Papercube.viewController.setContentToViewFromGUID(this._mouseDownGUID, 'Paper', NO); 743 }.bind(this); 744 745 var citeseerFunc = function(evt) 746 { 747 window.open(Papercube.Paper.find(this._mouseDownGUID).get('url')); 748 }.bind(this); 749 750 var saveFunc = function(evt) 751 { 752 Papercube.viewController.saveObject(this._mouseDownGUID, 'Paper'); 753 }.bind(this); 754 755 var zoomOutFunc = function(evt) 756 { 757 Papercube.canvasController.setZoomToPointerLocation(Event.pointerX(evt), Event.pointerY(evt), NO); 758 }.bind(this); 759 var zoomInFunc = function(evt) 760 { 761 Papercube.canvasController.setZoomToPointerLocation(Event.pointerX(evt), Event.pointerY(evt), YES); 762 }.bind(this); 763 764 Papercube.canvasController.registerFans('papertree', 765 { 766 focusedFan: { 767 CiteSeer: citeseerFunc, 768 Save: saveFunc, 769 "Zoom +": zoomInFunc, 770 "Zoom -": zoomOutFunc 771 }, 772 unfocusedFan: { 773 CiteSeer: citeseerFunc, 774 Save: saveFunc, 775 Refocus: refocusFunc, 776 "Zoom +": zoomInFunc, 777 "Zoom -": zoomOutFunc 778 } 779 }); 780 781 this._node = document.createElement('div'); 782 this._textNode = document.createElement('div'); 783 this._textNode.className = 'item-text'; 784 785 // make a new stylesheet 786 var ns = document.createElement('style'); 787 document.getElementsByTagName('head')[0].appendChild(ns); 788 789 // Safari does not see the new stylesheet unless you append something. 790 // However! IE will blow chunks, so ... filter it thusly: 791 if (!window.createPopup) { 792 ns.appendChild(document.createTextNode('')); 793 } 794 var s = this._styleSheet = document.styleSheets[document.styleSheets.length - 1]; 795 796 var rules = { 797 ".paper_tree_view_tab .item": "{ line-height:10px; position: absolute; border:1px solid #666; cursor:default;} ", 798 ".paper_tree_view_tab .item-text": "{ text-align:center; padding-top:10px; font-size: 9px; cursor:default; overflow: hidden;} " 799 }; 800 801 // loop through and insert 802 for (selector in rules) { 803 if (s.insertRule) { 804 // it's an IE browser 805 try { 806 s.insertRule(selector + rules[selector], s.cssRules.length); 807 } catch(e) {} 808 } else { 809 // it's a W3C browser 810 try { 811 s.addRule(selector, rules[selector]); 812 } catch(e) {} 813 } 814 } 815 816 sc_super(); 817 818 } 819 }) ; 820