1/** 2 * Copyright 2012 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17/** 18 * @fileoverview Extends OverlayView to provide a canvas "Layer". 19 * @author Brendan Kenny 20 */ 21 22/** 23 * A map layer that provides a canvas over the slippy map and a callback 24 * system for efficient animation. Requires canvas and CSS 2D transform 25 * support. 26 * @constructor 27 * @extends google.maps.OverlayView 28 * @param {CanvasLayerOptions=} opt_options Options to set in this CanvasLayer. 29 */ 30function CanvasLayer(opt_options) { 31 /** 32 * If true, canvas is in a map pane and the OverlayView is fully functional. 33 * See google.maps.OverlayView.onAdd for more information. 34 * @type {boolean} 35 * @private 36 */ 37 this.isAdded_ = false; 38 39 /** 40 * If true, each update will immediately schedule the next. 41 * @type {boolean} 42 * @private 43 */ 44 this.isAnimated_ = false; 45 46 /** 47 * The name of the MapPane in which this layer will be displayed. 48 * @type {string} 49 * @private 50 */ 51 this.paneName_ = CanvasLayer.DEFAULT_PANE_NAME_; 52 53 /** 54 * A user-supplied function called whenever an update is required. Null or 55 * undefined if a callback is not provided. 56 * @type {?function=} 57 * @private 58 */ 59 this.updateHandler_ = null; 60 61 /** 62 * A user-supplied function called whenever an update is required and the 63 * map has been resized since the last update. Null or undefined if a 64 * callback is not provided. 65 * @type {?function} 66 * @private 67 */ 68 this.resizeHandler_ = null; 69 70 /** 71 * The LatLng coordinate of the top left of the current view of the map. Will 72 * be null when this.isAdded_ is false. 73 * @type {google.maps.LatLng} 74 * @private 75 */ 76 this.topLeft_ = null; 77 78 /** 79 * The map-pan event listener. Will be null when this.isAdded_ is false. Will 80 * be null when this.isAdded_ is false. 81 * @type {?function} 82 * @private 83 */ 84 this.centerListener_ = null; 85 86 /** 87 * The map-resize event listener. Will be null when this.isAdded_ is false. 88 * @type {?function} 89 * @private 90 */ 91 this.resizeListener_ = null; 92 93 /** 94 * If true, the map size has changed and this.resizeHandler_ must be called 95 * on the next update. 96 * @type {boolean} 97 * @private 98 */ 99 this.needsResize_ = true; 100 101 /** 102 * A browser-defined id for the currently requested callback. Null when no 103 * callback is queued. 104 * @type {?number} 105 * @private 106 */ 107 this.requestAnimationFrameId_ = null; 108 109 var canvas = document.createElement('canvas'); 110 canvas.style.position = 'absolute'; 111 canvas.style.top = 0; 112 canvas.style.left = 0; 113 canvas.style.pointerEvents = 'none'; 114 115 /** 116 * The canvas element. 117 * @type {!HTMLCanvasElement} 118 */ 119 this.canvas = canvas; 120 121 /** 122 * The CSS width of the canvas, which may be different than the width of the 123 * backing store. 124 * @private {number} 125 */ 126 this.canvasCssWidth_ = 300; 127 128 /** 129 * The CSS height of the canvas, which may be different than the height of 130 * the backing store. 131 * @private {number} 132 */ 133 this.canvasCssHeight_ = 150; 134 135 /** 136 * A value for scaling the CanvasLayer resolution relative to the CanvasLayer 137 * display size. 138 * @private {number} 139 */ 140 this.resolutionScale_ = 1; 141 142 /** 143 * Simple bind for functions with no args for bind-less browsers (Safari). 144 * @param {Object} thisArg The this value used for the target function. 145 * @param {function} func The function to be bound. 146 */ 147 function simpleBindShim(thisArg, func) { 148 return function() { func.apply(thisArg); }; 149 } 150 151 /** 152 * A reference to this.repositionCanvas_ with this bound as its this value. 153 * @type {function} 154 * @private 155 */ 156 this.repositionFunction_ = simpleBindShim(this, this.repositionCanvas_); 157 158 /** 159 * A reference to this.resize_ with this bound as its this value. 160 * @type {function} 161 * @private 162 */ 163 this.resizeFunction_ = simpleBindShim(this, this.resize_); 164 165 /** 166 * A reference to this.update_ with this bound as its this value. 167 * @type {function} 168 * @private 169 */ 170 this.requestUpdateFunction_ = simpleBindShim(this, this.update_); 171 172 // set provided options, if any 173 if (opt_options) { 174 this.setOptions(opt_options); 175 } 176} 177 178if(window.google) 179 CanvasLayer.prototype = new google.maps.OverlayView(); 180 181/** 182 * The default MapPane to contain the canvas. 183 * @type {string} 184 * @const 185 * @private 186 */ 187CanvasLayer.DEFAULT_PANE_NAME_ = 'overlayLayer'; 188 189/** 190 * Transform CSS property name, with vendor prefix if required. If browser 191 * does not support transforms, property will be ignored. 192 * @type {string} 193 * @const 194 * @private 195 */ 196CanvasLayer.CSS_TRANSFORM_ = (function() { 197 var div = document.createElement('div'); 198 var transformProps = [ 199 'transform', 200 'WebkitTransform', 201 'MozTransform', 202 'OTransform', 203 'msTransform' 204 ]; 205 for (var i = 0; i < transformProps.length; i++) { 206 var prop = transformProps[i]; 207 if (div.style[prop] !== undefined) { 208 return prop; 209 } 210 } 211 212 // return unprefixed version by default 213 return transformProps[0]; 214})(); 215 216/** 217 * The requestAnimationFrame function, with vendor-prefixed or setTimeout-based 218 * fallbacks. MUST be called with window as thisArg. 219 * @type {function} 220 * @param {function} callback The function to add to the frame request queue. 221 * @return {number} The browser-defined id for the requested callback. 222 * @private 223 */ 224CanvasLayer.prototype.requestAnimFrame_ = 225 window.requestAnimationFrame || 226 window.webkitRequestAnimationFrame || 227 window.mozRequestAnimationFrame || 228 window.oRequestAnimationFrame || 229 window.msRequestAnimationFrame || 230 function(callback) { 231 return window.setTimeout(callback, 1000 / 60); 232 }; 233 234/** 235 * The cancelAnimationFrame function, with vendor-prefixed fallback. Does not 236 * fall back to clearTimeout as some platforms implement requestAnimationFrame 237 * but not cancelAnimationFrame, and the cost is an extra frame on onRemove. 238 * MUST be called with window as thisArg. 239 * @type {function} 240 * @param {number=} requestId The id of the frame request to cancel. 241 * @private 242 */ 243CanvasLayer.prototype.cancelAnimFrame_ = 244 window.cancelAnimationFrame || 245 window.webkitCancelAnimationFrame || 246 window.mozCancelAnimationFrame || 247 window.oCancelAnimationFrame || 248 window.msCancelAnimationFrame || 249 function(requestId) {}; 250 251/** 252 * Sets any options provided. See CanvasLayerOptions for more information. 253 * @param {CanvasLayerOptions} options The options to set. 254 */ 255CanvasLayer.prototype.setOptions = function(options) { 256 if (options.animate !== undefined) { 257 this.setAnimate(options.animate); 258 } 259 260 if (options.paneName !== undefined) { 261 this.setPaneName(options.paneName); 262 } 263 264 if (options.updateHandler !== undefined) { 265 this.setUpdateHandler(options.updateHandler); 266 } 267 268 if (options.resizeHandler !== undefined) { 269 this.setResizeHandler(options.resizeHandler); 270 } 271 272 if (options.resolutionScale !== undefined) { 273 this.setResolutionScale(options.resolutionScale); 274 } 275 276 if (options.map !== undefined) { 277 this.setMap(options.map); 278 } 279}; 280 281/** 282 * Set the animated state of the layer. If true, updateHandler will be called 283 * repeatedly, once per frame. If false, updateHandler will only be called when 284 * a map property changes that could require the canvas content to be redrawn. 285 * @param {boolean} animate Whether the canvas is animated. 286 */ 287CanvasLayer.prototype.setAnimate = function(animate) { 288 this.isAnimated_ = !!animate; 289 290 if (this.isAnimated_) { 291 this.scheduleUpdate(); 292 } 293}; 294 295/** 296 * @return {boolean} Whether the canvas is animated. 297 */ 298CanvasLayer.prototype.isAnimated = function() { 299 return this.isAnimated_; 300}; 301 302/** 303 * Set the MapPane in which this layer will be displayed, by name. See 304 * {@code google.maps.MapPanes} for the panes available. 305 * @param {string} paneName The name of the desired MapPane. 306 */ 307CanvasLayer.prototype.setPaneName = function(paneName) { 308 this.paneName_ = paneName; 309 310 this.setPane_(); 311}; 312 313/** 314 * @return {string} The name of the current container pane. 315 */ 316CanvasLayer.prototype.getPaneName = function() { 317 return this.paneName_; 318}; 319 320/** 321 * Adds the canvas to the specified container pane. Since this is guaranteed to 322 * execute only after onAdd is called, this is when paneName's existence is 323 * checked (and an error is thrown if it doesn't exist). 324 * @private 325 */ 326CanvasLayer.prototype.setPane_ = function() { 327 if (!this.isAdded_) { 328 return; 329 } 330 331 // onAdd has been called, so panes can be used 332 var panes = this.getPanes(); 333 if (!panes[this.paneName_]) { 334 throw new Error('"' + this.paneName_ + '" is not a valid MapPane name.'); 335 } 336 337 panes[this.paneName_].appendChild(this.canvas); 338}; 339 340/** 341 * Set a function that will be called whenever the parent map and the overlay's 342 * canvas have been resized. If opt_resizeHandler is null or unspecified, any 343 * existing callback is removed. 344 * @param {?function=} opt_resizeHandler The resize callback function. 345 */ 346CanvasLayer.prototype.setResizeHandler = function(opt_resizeHandler) { 347 this.resizeHandler_ = opt_resizeHandler; 348}; 349 350/** 351 * Sets a value for scaling the canvas resolution relative to the canvas 352 * display size. This can be used to save computation by scaling the backing
353 * buffer down, or to support high DPI devices by scaling it up (by e.g. 354 * window.devicePixelRatio). 355 * @param {number} scale 356 */ 357CanvasLayer.prototype.setResolutionScale = function(scale) { 358 if (typeof scale === 'number') { 359 this.resolutionScale_ = scale; 360 this.resize_(); 361 } 362}; 363 364/** 365 * Set a function that will be called when a repaint of the canvas is required. 366 * If opt_updateHandler is null or unspecified, any existing callback is 367 * removed. 368 * @param {?function=} opt_updateHandler The update callback function. 369 */ 370CanvasLayer.prototype.setUpdateHandler = function(opt_updateHandler) { 371 this.updateHandler_ = opt_updateHandler; 372}; 373 374/** 375 * @inheritDoc 376 */ 377CanvasLayer.prototype.onAdd = function() { 378 if (this.isAdded_) { 379 return; 380 } 381 382 this.isAdded_ = true; 383 this.setPane_(); 384 385 this.resizeListener_ = google.maps.event.addListener(this.getMap(), 386 'resize', this.resizeFunction_); 387 this.centerListener_ = google.maps.event.addListener(this.getMap(), 388 'center_changed', this.repositionFunction_); 389 390 this.resize_(); 391 this.repositionCanvas_(); 392}; 393 394/** 395 * @inheritDoc 396 */ 397CanvasLayer.prototype.onRemove = function() { 398 if (!this.isAdded_) { 399 return; 400 } 401 402 this.isAdded_ = false; 403 this.topLeft_ = null; 404 405 // remove canvas and listeners for pan and resize from map 406 this.canvas.parentElement.removeChild(this.canvas); 407 if (this.centerListener_) { 408 google.maps.event.removeListener(this.centerListener_); 409 this.centerListener_ = null; 410 } 411 if (this.resizeListener_) { 412 google.maps.event.removeListener(this.resizeListener_); 413 this.resizeListener_ = null; 414 } 415 416 // cease canvas update callbacks 417 if (this.requestAnimationFrameId_) { 418 this.cancelAnimFrame_.call(window, this.requestAnimationFrameId_); 419 this.requestAnimationFrameId_ = null; 420 } 421}; 422 423/** 424 * The internal callback for resize events that resizes the canvas to keep the 425 * map properly covered. 426 * @private 427 */ 428CanvasLayer.prototype.resize_ = function() { 429 if (!this.isAdded_) { 430 return; 431 } 432 433 var map = this.getMap(); 434 var mapWidth = map.getDiv().getElementsByTagName('div')[0].offsetWidth; 435 var mapHeight = map.getDiv().getElementsByTagName('div')[0].offsetHeight; 436 437 var newWidth = mapWidth * this.resolutionScale_; 438 var newHeight = mapHeight * this.resolutionScale_; 439 var oldWidth = this.canvas.width; 440 var oldHeight = this.canvas.height; 441 442 // resizing may allocate a new back buffer, so do so conservatively 443 if (oldWidth !== newWidth || oldHeight !== newHeight) { 444 this.canvas.width = newWidth; 445 this.canvas.height = newHeight; 446 447 this.needsResize_ = true; 448 this.scheduleUpdate(); 449 } 450 451 // reset styling if new sizes don't match; resize of data not needed 452 if (this.canvasCssWidth_ !== mapWidth || 453 this.canvasCssHeight_ !== mapHeight) { 454 this.canvasCssWidth_ = mapWidth; 455 this.canvasCssHeight_ = mapHeight; 456 this.canvas.style.width = mapWidth + 'px'; 457 this.canvas.style.height = mapHeight + 'px'; 458 } 459}; 460 461/** 462 * @inheritDoc 463 */ 464CanvasLayer.prototype.draw = function() { 465 this.repositionCanvas_(); 466}; 467 468/** 469 * Internal callback for map view changes. Since the Maps API moves the overlay 470 * along with the map, this function calculates the opposite translation to 471 * keep the canvas in place. 472 * @private 473 */ 474CanvasLayer.prototype.repositionCanvas_ = function() { 475 // TODO(bckenny): *should* only be executed on RAF, but in current browsers 476 // this causes noticeable hitches in map and overlay relative 477 // positioning. 478 479 var map = this.getMap(); 480 481 // topLeft can't be calculated from map.getBounds(), because bounds are 482 // clamped to -180 and 180 when completely zoomed out. Instead, calculate 483 // left as an offset from the center, which is an unwrapped LatLng. 484 var top = map.getBounds().getNorthEast().lat(); 485 var center = map.getCenter(); 486 var scale = Math.pow(2, map.getZoom()); 487 var left = center.lng() - (this.canvasCssWidth_ * 180) / (256 * scale); 488 this.topLeft_ = new google.maps.LatLng(top, left); 489 490 // Canvas position relative to draggable map's container depends on 491 // overlayView's projection, not the map's. Have to use the center of the 492 // map for this, not the top left, for the same reason as above. 493 var projection = this.getProjection(); 494 var divCenter = projection.fromLatLngToDivPixel(center); 495 var offsetX = -Math.round(this.canvasCssWidth_ / 2 - divCenter.x); 496 var offsetY = -Math.round(this.canvasCssHeight_ / 2 - divCenter.y); 497 this.canvas.style[CanvasLayer.CSS_TRANSFORM_] = 'translate(' + 498 offsetX + 'px,' + offsetY + 'px)'; 499 500 this.scheduleUpdate(); 501}; 502 503/**
504 * Internal callback that serves as main animation scheduler via 505 * requestAnimationFrame. Calls resize and update callbacks if set, and 506 * schedules the next frame if overlay is animated. 507 * @private 508 */ 509CanvasLayer.prototype.update_ = function() { 510 this.requestAnimationFrameId_ = null; 511 512 if (!this.isAdded_) { 513 return; 514 } 515 516 if (this.isAnimated_) { 517 this.scheduleUpdate(); 518 } 519 520 if (this.needsResize_ && this.resizeHandler_) { 521 this.needsResize_ = false; 522 this.resizeHandler_(); 523 } 524 525 if (this.updateHandler_) { 526 this.updateHandler_(); 527 } 528}; 529 530/** 531 * A convenience method to get the current LatLng coordinate of the top left of 532 * the current view of the map. 533 * @return {google.maps.LatLng} The top left coordinate. 534 */ 535CanvasLayer.prototype.getTopLeft = function() { 536 return this.topLeft_; 537}; 538 539/** 540 * Schedule a requestAnimationFrame callback to updateHandler. If one is 541 * already scheduled, there is no effect. 542 */ 543CanvasLayer.prototype.scheduleUpdate = function() { 544 if (this.isAdded_ && !this.requestAnimationFrameId_) { 545 this.requestAnimationFrameId_ = 546 this.requestAnimFrame_.call(window, this.requestUpdateFunction_); 547 } 548};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.