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