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