1 /** Creates a minimap for the big canvas
  2  *
  3 * @constructor
  4 * @this {Minimap}
  5 * @param {HTMLObject} bigCanvas (usually the canvas object)
  6 * @param {HTMLObject} minimapContainer The minimap DOM Object (usually the container DIV)
  7 * @author Zack Newsham <zack_newsham@yahoo.co.uk>
  8 * @author Alex Gheorghiu <alex@scriptoid.com>
  9 *
 10 * @see See /documents/specs/minimap.jpg for a visual representation of the minimap architecture.
 11 * @see See minimap.css to fully understand the CSS positioning
 12  */
 13 function Minimap(bigCanvas, minimapContainer){
 14     /**Keeps track it minimap is selected or not*/
 15     this.selected = false;
 16     
 17     /**The big canvas DOM object (canvas). You can get it also by document.getElementById('map')*/
 18     this.bigCanvas = bigCanvas;
 19     
 20     /**The minimap DOM object (div). You can get it also by document.getElementById('minimap')*/
 21     this.minimapContainer = minimapContainer;
 22 
 23     /*Stores reference to div selection rectangle (as a div)*/
 24     this.selection = document.createElement("div");
 25     
 26     
 27     this.selection.id = "selection";
 28 
 29     //create a canvas to paint the minimap on
 30     /**Reference to small DOM canvas (minimap)*/
 31     this.smallCanvas = document.createElement("canvas");
 32     this.smallCanvas.style.width = "100%";
 33     this.smallCanvas.style.height = "100%";
 34 
 35     //the size has to be specified so we get a minimap that has the same proportions as the map
 36     this.minimapContainer.style.height = Minimap.prefferedHeight + "px"; //initially it's zero
 37     this.minimapContainer.style.width = Minimap.prefferedWidth + "px";
 38 
 39     this.minimapContainer.appendChild(this.smallCanvas);
 40     this.minimapContainer.appendChild(this.selection);
 41 
 42 
 43     //If big canvas is scrolled --effect--> update minimap position
 44     this.bigCanvas.parentNode.onscroll = function (scrollObject){
 45         return function(event){
 46             scrollObject.updateMinimapPosition();
 47         }
 48     }(this);
 49 
 50 
 51     //Minimap move mouse --effect--> update map position
 52     this.minimapContainer.onmousemove = function(aMinimapObject){
 53         return function(event){
 54             aMinimapObject.onScrollMinimap(event);
 55             return false;
 56         }
 57     }(this);
 58 
 59 
 60     //Selection mouse down --effect--> select minimap
 61     this.selection.onmousedown = function(aMinimapObject){
 62         return function(event){
 63             /*prevent Firefox to allow canvas dragg effect. By default FF allows you
 64              * to drag the canvas out of it's place, similar to drag an image*/
 65             event.preventDefault();
 66             aMinimapObject.selected = true;
 67         }
 68     }(this);
 69 
 70     //Canvas mouse down --effect--> center selection
 71     this.smallCanvas.onmousedown = function(aMinimapObject){
 72         return function(event){
 73             aMinimapObject.selected = true;
 74             aMinimapObject.onScrollMinimap(event);
 75         }
 76     }(this);
 77 
 78 
 79     //because we have a fixed width this ratio will give us the (minimap width / bigmap width) percent
 80     /**The ratio between the big map and the small one*/
 81     this.ratio = 0;
 82 
 83     this.initMinimap();
 84 }
 85 
 86 
 87 /**
 88  *Scrollbar width
 89  *19px is the width added to a scollable area (Zack discovered this into Chrome)
 90  *We might compute this dimension too but for now it's fine
 91  *even if we are wrong by a pixel or two
 92  **/
 93 Minimap.scrollBarWidth = 19;
 94 
 95 /**Preffered/default width*/
 96 Minimap.prefferedWidth = 115;
 97 
 98 /**Preffered/default height*/
 99 Minimap.prefferedHeight = 250;
100 
101 Minimap.prototype = {
102 
103     /**
104      *Update the minimap (canvas) with a scalled down version of the big map (canvas)
105      *@author Zack
106      *TODO: because of this the whole paiting is very slow. We need to optimize it
107      *Ideea: make update not real time....but with a delay...1 s for example  so
108      *instead of hundreds of micro repaint we will have only a few
109      **/
110     updateMinimap:function(){
111         //this part should be moved somewhere more relevant, only here for testing
112         var canvas = this.bigCanvas;
113         var ctx = canvas.getContext("2d");
114 
115         //recreate a new image from encoded data
116         var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
117 
118         var thisCtx = this.smallCanvas.getContext("2d");
119         thisCtx.beginPath();
120         thisCtx.clearRect(0,0,this.smallCanvas.width,this.smallCanvas.height)
121         thisCtx.closePath();
122         thisCtx.stroke();
123         thisCtx.save();
124 
125         /*@see http://stackoverflow.com/questions/3448347/how-to-scale-an-imagedata-in-html-canvas*/
126         thisCtx.scale(this.ratio/100 , this.ratio/100 );
127         thisCtx.drawImage(canvas, 0,0);
128         thisCtx.restore();
129     },
130 
131 
132 
133     /**
134      *Reset/init ratio and minimap's height
135      *@author Zack, Alex
136      **/
137     initMinimap:function(){
138         /*
139          *We need to recompute the width and height of the minimap.
140          *Initially minimap has a Minimap.prefferedWidth x Minimap.prefferedHeight size
141          *Now we will compute the vertical ration and horizontal one
142          *and pick the smaller one as general ratio and based on that we will 
143          *recompute the width and height of the minimap
144          **/
145         var horizontalRatio = Minimap.prefferedWidth * 100 / ($(this.bigCanvas).width()); //horizontal ratio
146         
147         var verticalRatio = Minimap.prefferedHeight * 100 / ($(this.bigCanvas).height()); //vertical ratio
148         
149         
150         //pick smaller ratio
151         if(horizontalRatio < verticalRatio){
152             this.ratio = horizontalRatio;            
153         }
154         else{
155             this.ratio = verticalRatio;
156         }
157         
158         //recompute width and height
159         var width = $(this.bigCanvas).width()  * this.ratio / 100;
160         var height = $(this.bigCanvas).height()  * this.ratio / 100;
161         
162             
163         //update minimap container sizes
164         this.minimapContainer.style.width = width +"px";
165         this.minimapContainer.style.height = height + "px";
166 
167 
168         //small canvas will fill all it's parent space (it's a gas :) )
169         this.smallCanvas.width = $(this.minimapContainer).width();
170         this.smallCanvas.height = $(this.minimapContainer).height();
171 
172         //compute selection size
173         var selectionWidth = this.ratio * ($(this.bigCanvas.parentNode).width() - Minimap.scrollBarWidth) / 100;
174         var selectionHeight =  this.ratio * ($(this.bigCanvas.parentNode).height() - Minimap.scrollBarWidth) / 100;
175         if(selectionWidth > $(this.minimapContainer).width()){ //if selection bigger than the container trim it
176             selectionWidth = $(this.minimapContainer).width();
177         }
178         if(selectionHeight > $(this.minimapContainer).height()){ //if selection bigger than the container trim it
179             selectionHeight = $(this.minimapContainer).height();
180         }
181 
182         //update selection
183         this.selection.style.width = selectionWidth + "px";
184         this.selection.style.height = selectionHeight + "px";
185     },
186 
187 
188     /**Called by Minimap, usually it just moves/shift the big canvas into the right
189      *position
190      **/
191     updateMapPosition:function(){
192         var x = parseInt(this.selection.style.left.replace("px",""));//+border
193         var y = parseInt(this.selection.style.top.replace("px",""));//+border
194 
195         this.bigCanvas.parentNode.scrollLeft = x / this.ratio * 100;
196         this.bigCanvas.parentNode.scrollTop = y / this.ratio * 100;
197     },
198 
199 
200 
201     /**
202      *Called whenever we scroll the big map/canvas. It will update the minimap
203      *@author Zack
204      */
205     updateMinimapPosition:function(){
206         //get big map's offset
207         var x = parseInt(this.bigCanvas.parentNode.scrollLeft);
208         var y = parseInt(this.bigCanvas.parentNode.scrollTop);
209 
210         //compute minimap's offset
211         x = x * this.ratio / 100 ;
212         y = y * this.ratio / 100 ;
213 
214         //apply the offset
215         this.selection.style.left = x + "px";
216         this.selection.style.top = y + "px";
217     },
218 
219 
220 
221     /**Called when we move over the minimap and the 'minimap' was previously selected
222      *@param {Event} event - the event triggered
223      *@author Zack
224      **/
225     onScrollMinimap:function(event){
226         if(this.selected == true){ //we will 'action' only if the select are is selected
227 
228             //try to reposition the selection
229             var mousePos = this.getInternalXY(event);
230 
231             var containerWidth = this.minimapContainer.style.width.replace("px","");
232             var containerHeight = this.minimapContainer.style.height.replace("px","");
233             var width = this.selection.style.width.replace("px","");
234             var height = this.selection.style.height.replace("px","");
235 
236             //if we are scrolling outside the area, put us back in
237             if(mousePos[0] - width/2 < 0){
238                 mousePos[0] = width/2;
239             }
240             if(mousePos[1] - height/2 < 0){
241                 mousePos[1] = height/2;
242             }
243             if(mousePos[0] + width/2 > containerWidth){
244                 mousePos[0] = containerWidth - width/2;
245             }
246             if(mousePos[1] + height/2 > containerHeight){
247                 mousePos[1] = containerHeight - height/2;
248             }
249 
250             //update our minimap
251             if(mousePos[0] != undefined){
252                 this.selection.style.left = mousePos[0] - width/2 + "px";
253                 this.selection.style.top = mousePos[1] - height/2 + "px";
254             }
255 
256             //update the actual area
257             this.updateMapPosition();
258         }
259         else{
260             this.selected = false;
261         }
262     },
263 
264 
265     /**
266      *Computes the boundary of minimap relative to the whole page
267      *@author Zack
268      *@author (comments) Alex
269      *@see <a href="http://www.quirksmode.org/js/findpos.html">http://www.quirksmode.org/js/findpos.html</a>
270      **/
271     getBounds:function(){
272         var thisMinX = 0;
273         var thisMinY = 0;
274         var obj = this.minimapContainer;
275 
276         /*Go recursively up in the hierarchy (parent) and find the minx and min y*/
277         do{
278             thisMinX += obj.offsetLeft;
279             thisMinY += obj.offsetTop;
280 
281             /*offsetParent - Returns a reference to the object that is the current
282              *element's offset positioning context*/
283         }while(obj = obj.offsetParent);
284 
285         /*Add minimap's width and height*/
286         var thisMaxX = thisMinX + parseInt(this.minimapContainer.style.width.replace("px",""));
287         var thisMaxY = thisMinY + parseInt(this.minimapContainer.style.height.replace("px",""));
288 
289         return [thisMinX, thisMinY, thisMaxX, thisMaxY];
290     },
291 
292 
293     /**Get the (x, y) position relative to
294      *current DOM object (minimap div in our case)
295      *@param {Event} event - the event triggered
296      *@return {Array} of [x, y] relative position inside DOM object
297      *@author Zack
298      *@author (comments) Alex
299      **/
300     getInternalXY:function(event){
301         var position = [];
302 
303         var thisBounds = this.getBounds();
304         if(event.pageX >= thisBounds[0] && event.pageX <= thisBounds[2] //if event inside [Ox bounds
305             && event.pageY >= thisBounds[1] && event.pageY <= thisBounds[3]) //if event inside [Oy bounds
306             {
307             position = [event.pageX - thisBounds[0], event.pageY - thisBounds[1]];
308         }
309 
310         return position;
311     }
312 }