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 * Modification Notice 24 * Author: WP Go Maps Development Team (CodeCabin) 25 * Date: 2024-03-13 26 * 27 * Details: 28 * We have altered the way the canvas extension happens, allowing it to be delayed in the event that the Google Maps API is loaded in async mode 29 * this is done in order to account for the load delays when loading sub-modules from Google API's, which may cause some of this module 30 * to fail when loaded into the DOM 31 * 32 * The solution, for the interim is to attempt the extension, and if google is not yet defined, delay this by 500ms 33 * before retrying the extension. The extension will be retried a total of 10 times at most, before it fails in it's entirety 34 * 35 * No notice will be provided in this case, but a flag will be set on the CanvasLayer definition itself 36 */ 37 38/** 39 * A map layer that provides a canvas over the slippy map and a callback 40 * system for efficient animation. Requires canvas and CSS 2D transform 41 * support. 42 * @constructor 43 * @extends google.maps.OverlayView 44 * @param {CanvasLayerOptions=} opt_options Options to set in this CanvasLayer. 45 */ 46function CanvasLayer(opt_options) { 47 /** 48 * If true, canvas is in a map pane and the OverlayView is fully functional. 49 * See google.maps.OverlayView.onAdd for more information. 50 * @type {boolean} 51 * @private 52 */ 53 this.isAdded_ = false; 54 55 /** 56 * If true, each update will immediately schedule the next. 57 * @type {boolean} 58 * @private 59 */ 60 this.isAnimated_ = false; 61 62 /** 63 * The name of the MapPane in which this layer will be displayed. 64 * @type {string} 65 * @private 66 */ 67 this.paneName_ = CanvasLayer.DEFAULT_PANE_NAME_; 68 69 /** 70 * A user-supplied function called whenever an update is required. Null or 71 * undefined if a callback is not provided. 72 * @type {?function=} 73 * @private 74 */ 75 this.updateHandler_ = null; 76 77 /** 78 * A user-supplied function called whenever an update is required and the 79 * map has been resized since the last update. Null or undefined if a 80 * callback is not provided. 81 * @type {?function} 82 * @private 83 */ 84 this.resizeHandler_ = null; 85 86 /** 87 * The LatLng coordinate of the top left of the current view of the map. Will 88 * be null when this.isAdded_ is false. 89 * @type {google.maps.LatLng} 90 * @private 91 */ 92 this.topLeft_ = null; 93 94 /** 95 * The map-pan event listener. Will be null when this.isAdded_ is false. Will 96 * be null when this.isAdded_ is false. 97 * @type {?function} 98 * @private 99 */ 100 this.centerListener_ = null; 101 102 /** 103 * The map-resize event listener. Will be null when this.isAdded_ is false. 104 * @type {?function} 105 * @private 106 */ 107 this.resizeListener_ = null; 108 109 /** 110 * If true, the map size has changed and this.resizeHandler_ must be called 111 * on the next update. 112 * @type {boolean} 113 * @private 114 */ 115 this.needsResize_ = true; 116 117 /** 118 * A browser-defined id for the currently requested callback. Null when no 119 * callback is queued. 120 * @type {?number} 121 * @private 122 */ 123 this.requestAnimationFrameId_ = null; 124 125 var canvas = document.createElement('canvas'); 126 canvas.style.position = 'absolute'; 127 canvas.style.top = 0; 128 canvas.style.left = 0; 129 canvas.style.pointerEvents = 'none'; 130 131 /** 132 * The canvas element. 133 * @type {!HTMLCanvasElement} 134 */ 135 this.canvas = canvas; 136 137 /** 138 * The CSS width of the canvas, which may be different than the width of the 139 * backing store. 140 * @private {number} 141 */
142 this.canvasCssWidth_ = 300; 143 144 /** 145 * The CSS height of the canvas, which may be different than the height of 146 * the backing store. 147 * @private {number} 148 */ 149 this.canvasCssHeight_ = 150; 150 151 /** 152 * A value for scaling the CanvasLayer resolution relative to the CanvasLayer 153 * display size. 154 * @private {number} 155 */ 156 this.resolutionScale_ = 1; 157 158 /** 159 * Simple bind for functions with no args for bind-less browsers (Safari). 160 * @param {Object} thisArg The this value used for the target function. 161 * @param {function} func The function to be bound. 162 */ 163 function simpleBindShim(thisArg, func) { 164 return function() { func.apply(thisArg); }; 165 } 166 167 /** 168 * A reference to this.repositionCanvas_ with this bound as its this value. 169 * @type {function} 170 * @private 171 */ 172 this.repositionFunction_ = simpleBindShim(this, this.repositionCanvas_); 173 174 /** 175 * A reference to this.resize_ with this bound as its this value. 176 * @type {function} 177 * @private 178 */ 179 this.resizeFunction_ = simpleBindShim(this, this.resize_); 180 181 /** 182 * A reference to this.update_ with this bound as its this value. 183 * @type {function} 184 * @private 185 */ 186 this.requestUpdateFunction_ = simpleBindShim(this, this.update_); 187 188 // set provided options, if any 189 if (opt_options) { 190 this.setOptions(opt_options); 191 } 192} 193 194/** 195 * A very simple way to track the extension status 196 */ 197CanvasLayer.__EXTENSION_STATUS = { 198 attempts : 0, 199 max : 10, 200 delay : 500, 201 extended : false 202}; 203 204/** 205 * A somewhat recursive prototype extension module developed to allow async loading within the Google Maps API 206 * 207 * Since the original module was written some technical specifications for the Google Maps platform have changed, 208 * so by accounting for this in an admittedly rudimentary way, we can allow async loading, without changing the surround code loading order 209*/ 210function __canvasLayerPrototypeExtension(){ 211 CanvasLayer.__EXTENSION_STATUS.attempts += 1; 212 213 if(typeof window.google !== 'undefined' && typeof window.google.maps !== 'undefined' && typeof window.google.maps.OverlayView !== 'undefined'){ 214 /* Perform the extension, this code is unchanged from the original, and simply moved into this callable */ 215 CanvasLayer.__EXTENSION_STATUS.extended = true; 216 217 if(window.google) 218 CanvasLayer.prototype = new google.maps.OverlayView(); 219 220 /** 221 * The default MapPane to contain the canvas. 222 * @type {string} 223 * @const 224 * @private 225 */ 226 CanvasLayer.DEFAULT_PANE_NAME_ = 'overlayLayer'; 227 228 /** 229 * Transform CSS property name, with vendor prefix if required. If browser 230 * does not support transforms, property will be ignored. 231 * @type {string} 232 * @const 233 * @private 234 */ 235 CanvasLayer.CSS_TRANSFORM_ = (function() { 236 var div = document.createElement('div'); 237 var transformProps = [ 238 'transform', 239 'WebkitTransform', 240 'MozTransform', 241 'OTransform', 242 'msTransform' 243 ]; 244 for (var i = 0; i < transformProps.length; i++) { 245 var prop = transformProps[i]; 246 if (div.style[prop] !== undefined) { 247 return prop; 248 } 249 } 250 251 // return unprefixed version by default 252 return transformProps[0]; 253 })(); 254 255 /** 256 * The requestAnimationFrame function, with vendor-prefixed or setTimeout-based 257 * fallbacks. MUST be called with window as thisArg. 258 * @type {function} 259 * @param {function} callback The function to add to the frame request queue. 260 * @return {number} The browser-defined id for the requested callback. 261 * @private 262 */ 263 CanvasLayer.prototype.requestAnimFrame_ = 264 window.requestAnimationFrame || 265 window.webkitRequestAnimationFrame || 266 window.mozRequestAnimationFrame || 267 window.oRequestAnimationFrame || 268 window.msRequestAnimationFrame || 269 function(callback) { 270 return window.setTimeout(callback, 1000 / 60); 271 }; 272 273 /** 274 * The cancelAnimationFrame function, with vendor-prefixed fallback. Does not 275 * fall back to clearTimeout as some platforms implement requestAnimationFrame 276 * but not cancelAnimationFrame, and the cost is an extra frame on onRemove. 277 * MUST be called with window as thisArg. 278 * @type {function} 279 * @param {number=} requestId The id of the frame request to cancel. 280 * @private 281 */ 282 CanvasLayer.prototype.cancelAnimFrame_ = 283 window.cancelAnimationFrame || 284 window.webkitCancelAnimationFrame || 285 window.mozCancelAnimationFrame || 286 window.oCancelAnimationFrame || 287 window.msCancelAnimationFrame || 288 function(requestId) {}; 289 290 /** 291 * Sets any options provided. See CanvasLayerOptions for more information. 292 * @param {CanvasLayerOptions} options The options to set. 293 */ 294 CanvasLayer.prototype.setOptions = function(options) { 295 if (options.animate !== undefined) { 296 this.setAnimate(options.animate); 297 } 298 299 if (options.paneName !== undefined) { 300 this.setPaneName(options.paneName); 301 } 302 303 if (options.updateHandler !== undefined) { 304 this.setUpdateHandler(options.updateHandler); 305 } 306 307 if (options.resizeHandler !== undefined) { 308 this.setResizeHandler(options.resizeHandler); 309 } 310 311 if (options.resolutionScale !== undefined) { 312 this.setResolutionScale(options.resolutionScale); 313 } 314 315 if (options.map !== undefined) { 316 this.setMap(options.map); 317 } 318 }; 319 320 /** 321 * Set the animated state of the layer. If true, updateHandler will be called 322 * repeatedly, once per frame. If false, updateHandler will only be called when 323 * a map property changes that could require the canvas content to be redrawn. 324 * @param {boolean} animate Whether the canvas is animated. 325 */ 326 CanvasLayer.prototype.setAnimate = function(animate) { 327 this.isAnimated_ = !!animate; 328 329 if (this.isAnimated_) { 330 this.scheduleUpdate(); 331 } 332 }; 333 334 /** 335 * @return {boolean} Whether the canvas is animated. 336 */ 337 CanvasLayer.prototype.isAnimated = function() { 338 return this.isAnimated_; 339 }; 340 341 /** 342 * Set the MapPane in which this layer will be displayed, by name. See 343 * {@code google.maps.MapPanes} for the panes available. 344 * @param {string} paneName The name of the desired MapPane. 345 */ 346 CanvasLayer.prototype.setPaneName = function(paneName) { 347 this.paneName_ = paneName; 348 349 this.setPane_(); 350 }; 351 352 /** 353 * @return {string} The name of the current container pane. 354 */ 355 CanvasLayer.prototype.getPaneName = function() { 356 return this.paneName_; 357 }; 358 359 /** 360 * Adds the canvas to the specified container pane. Since this is guaranteed to 361 * execute only after onAdd is called, this is when paneName's existence is 362 * checked (and an error is thrown if it doesn't exist). 363 * @private 364 */ 365 CanvasLayer.prototype.setPane_ = function() { 366 if (!this.isAdded_) { 367 return; 368 } 369 370 // onAdd has been called, so panes can be used 371 var panes = this.getPanes(); 372 if (!panes[this.paneName_]) { 373 throw new Error('"' + this.paneName_ + '" is not a valid MapPane name.'); 374 } 375 376 panes[this.paneName_].appendChild(this.canvas); 377 }; 378 379 /** 380 * Set a function that will be called whenever the parent map and the overlay's 381 * canvas have been resized. If opt_resizeHandler is null or unspecified, any 382 * existing callback is removed. 383 * @param {?function=} opt_resizeHandler The resize callback function. 384 */ 385 CanvasLayer.prototype.setResizeHandler = function(opt_resizeHandler) { 386 this.resizeHandler_ = opt_resizeHandler; 387 }; 388 389 /** 390 * Sets a value for scaling the canvas resolution relative to the canvas 391 * display size. This can be used to save computation by scaling the backing
392 * buffer down, or to support high DPI devices by scaling it up (by e.g. 393 * window.devicePixelRatio). 394 * @param {number} scale 395 */ 396 CanvasLayer.prototype.setResolutionScale = function(scale) { 397 if (typeof scale === 'number') { 398 this.resolutionScale_ = scale; 399 this.resize_(); 400 } 401 }; 402 403 /** 404 * Set a function that will be called when a repaint of the canvas is required. 405 * If opt_updateHandler is null or unspecified, any existing callback is 406 * removed. 407 * @param {?function=} opt_updateHandler The update callback function. 408 */ 409 CanvasLayer.prototype.setUpdateHandler = function(opt_updateHandler) { 410 this.updateHandler_ = opt_updateHandler; 411 }; 412 413 /** 414 * @inheritDoc 415 */ 416 CanvasLayer.prototype.onAdd = function() { 417 if (this.isAdded_) { 418 return; 419 } 420 421 this.isAdded_ = true; 422 this.setPane_(); 423 424 this.resizeListener_ = google.maps.event.addListener(this.getMap(), 425 'resize', this.resizeFunction_); 426 this.centerListener_ = google.maps.event.addListener(this.getMap(), 427 'center_changed', this.repositionFunction_); 428 429 this.resize_(); 430 this.repositionCanvas_(); 431 }; 432 433 /** 434 * @inheritDoc 435 */ 436 CanvasLayer.prototype.onRemove = function() { 437 if (!this.isAdded_) { 438 return; 439 } 440 441 this.isAdded_ = false; 442 this.topLeft_ = null; 443 444 // remove canvas and listeners for pan and resize from map 445 this.canvas.parentElement.removeChild(this.canvas); 446 if (this.centerListener_) { 447 google.maps.event.removeListener(this.centerListener_); 448 this.centerListener_ = null; 449 } 450 if (this.resizeListener_) { 451 google.maps.event.removeListener(this.resizeListener_); 452 this.resizeListener_ = null; 453 } 454 455 // cease canvas update callbacks 456 if (this.requestAnimationFrameId_) { 457 this.cancelAnimFrame_.call(window, this.requestAnimationFrameId_); 458 this.requestAnimationFrameId_ = null; 459 } 460 }; 461 462 /** 463 * The internal callback for resize events that resizes the canvas to keep the 464 * map properly covered. 465 * @private 466 */ 467 CanvasLayer.prototype.resize_ = function() { 468 if (!this.isAdded_) { 469 return; 470 } 471 472 var map = this.getMap(); 473 var mapWidth = map.getDiv().getElementsByTagName('div')[0].offsetWidth; 474 var mapHeight = map.getDiv().getElementsByTagName('div')[0].offsetHeight; 475 476 var newWidth = mapWidth * this.resolutionScale_; 477 var newHeight = mapHeight * this.resolutionScale_; 478 var oldWidth = this.canvas.width; 479 var oldHeight = this.canvas.height; 480 481 // resizing may allocate a new back buffer, so do so conservatively 482 if (oldWidth !== newWidth || oldHeight !== newHeight) { 483 this.canvas.width = newWidth; 484 this.canvas.height = newHeight; 485 486 this.needsResize_ = true; 487 this.scheduleUpdate(); 488 } 489 490 // reset styling if new sizes don't match; resize of data not needed 491 if (this.canvasCssWidth_ !== mapWidth || 492 this.canvasCssHeight_ !== mapHeight) { 493 this.canvasCssWidth_ = mapWidth; 494 this.canvasCssHeight_ = mapHeight; 495 this.canvas.style.width = mapWidth + 'px'; 496 this.canvas.style.height = mapHeight + 'px'; 497 } 498 }; 499 500 /** 501 * @inheritDoc 502 */ 503 CanvasLayer.prototype.draw = function() { 504 this.repositionCanvas_(); 505 }; 506 507 /** 508 * Internal callback for map view changes. Since the Maps API moves the overlay 509 * along with the map, this function calculates the opposite translation to 510 * keep the canvas in place. 511 * @private 512 */ 513 CanvasLayer.prototype.repositionCanvas_ = function() { 514 // TODO(bckenny): *should* only be executed on RAF, but in current browsers 515 // this causes noticeable hitches in map and overlay relative 516 // positioning. 517 518 var map = this.getMap(); 519 520 // topLeft can't be calculated from map.getBounds(), because bounds are 521 // clamped to -180 and 180 when completely zoomed out. Instead, calculate 522 // left as an offset from the center, which is an unwrapped LatLng. 523 var top = map.getBounds().getNorthEast().lat(); 524 var center = map.getCenter(); 525 var scale = Math.pow(2, map.getZoom()); 526 var left = center.lng() - (this.canvasCssWidth_ * 180) / (256 * scale); 527 this.topLeft_ = new google.maps.LatLng(top, left); 528 529 // Canvas position relative to draggable map's container depends on 530 // overlayView's projection, not the map's. Have to use the center of the 531 // map for this, not the top left, for the same reason as above. 532 var projection = this.getProjection(); 533 var divCenter = projection.fromLatLngToDivPixel(center); 534 var offsetX = -Math.round(this.canvasCssWidth_ / 2 - divCenter.x); 535 var offsetY = -Math.round(this.canvasCssHeight_ / 2 - divCenter.y); 536 this.canvas.style[CanvasLayer.CSS_TRANSFORM_] = 'translate(' + 537 offsetX + 'px,' + offsetY + 'px)'; 538 539 this.scheduleUpdate(); 540 }; 541 542 /**
543 * Internal callback that serves as main animation scheduler via 544 * requestAnimationFrame. Calls resize and update callbacks if set, and 545 * schedules the next frame if overlay is animated. 546 * @private 547 */ 548 CanvasLayer.prototype.update_ = function() { 549 this.requestAnimationFrameId_ = null; 550 551 if (!this.isAdded_) { 552 return; 553 } 554 555 if (this.isAnimated_) { 556 this.scheduleUpdate(); 557 } 558 559 if (this.needsResize_ && this.resizeHandler_) { 560 this.needsResize_ = false; 561 this.resizeHandler_(); 562 } 563 564 if (this.updateHandler_) { 565 this.updateHandler_(); 566 } 567 }; 568 569 /** 570 * A convenience method to get the current LatLng coordinate of the top left of 571 * the current view of the map. 572 * @return {google.maps.LatLng} The top left coordinate. 573 */ 574 CanvasLayer.prototype.getTopLeft = function() { 575 return this.topLeft_; 576 }; 577 578 /** 579 * Schedule a requestAnimationFrame callback to updateHandler. If one is 580 * already scheduled, there is no effect. 581 */ 582 CanvasLayer.prototype.scheduleUpdate = function() { 583 if (this.isAdded_ && !this.requestAnimationFrameId_) { 584 this.requestAnimationFrameId_ = 585 this.requestAnimFrame_.call(window, this.requestUpdateFunction_); 586 } 587 }; 588 589 } else { 590 /* Queue this for 500ms from now to allow us to retry until we get it loaded */ 591 if(CanvasLayer.__EXTENSION_STATUS.attempts < CanvasLayer.__EXTENSION_STATUS.max){ 592 setTimeout(() => { 593 __canvasLayerPrototypeExtension(); 594 }, CanvasLayer.__EXTENSION_STATUS.delay); 595 } 596 } 597} 598 599// Start the loader 600__canvasLayerPrototypeExtension(); 601
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.