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 }