1// Based on https://github.com/shramov/leaflet-plugins 2// GridLayer like https://avinmathew.com/leaflet-and-google-maps/ , but using MutationObserver instead of jQuery 3 4 5// ðclass GridLayer.GoogleMutant 6// ðextends GridLayer 7L.GridLayer.GoogleMutant = L.GridLayer.extend({ 8 options: { 9 minZoom: 0, 10 maxZoom: 23, 11 tileSize: 256, 12 subdomains: 'abc', 13 errorTileUrl: '', 14 attribution: '', // The mutant container will add its own attribution anyways. 15 opacity: 1, 16 continuousWorld: false, 17 noWrap: false, 18 // ðoption type: String = 'roadmap' 19 // Google's map type. Valid values are 'roadmap', 'satellite' or 'terrain'. 'hybrid' is not really supported. 20 type: 'roadmap', 21 maxNativeZoom: 21 22 }, 23 24 initialize: function (options) { 25 L.GridLayer.prototype.initialize.call(this, options); 26 27 this._ready = !!window.google && !!window.google.maps && !!window.google.maps.Map; 28 29 this._GAPIPromise = this._ready ? Promise.resolve(window.google) : new Promise(function (resolve, reject) { 30 var checkCounter = 0; 31 var intervalId = null; 32 intervalId = setInterval(function () { 33 if (checkCounter >= 10) { 34 clearInterval(intervalId); 35 return reject(new Error('window.google not found after 10 attempts')); 36 } 37 if (!!window.google && !!window.google.maps && !!window.google.maps.Map) { 38 clearInterval(intervalId); 39 return resolve(window.google); 40 } 41 checkCounter++; 42 }, 500); 43 }); 44 45 // Couple data structures indexed by tile key 46 this._tileCallbacks = {}; // Callbacks for promises for tiles that are expected 47 this._freshTiles = {}; // Tiles from the mutant which haven't been requested yet 48 49 this._imagesPerTile = (this.options.type === 'hybrid') ? 2 : 1; 50 51 this._boundOnMutatedImage = this._onMutatedImage.bind(this); 52 }, 53 54 onAdd: function (map) { 55 L.GridLayer.prototype.onAdd.call(this, map); 56 this._initMutantContainer(); 57 58 this._GAPIPromise.then(function () { 59 this._ready = true; 60 this._map = map; 61 62 this._initMutant(); 63 64 map.on('viewreset', this._reset, this); 65 if (this.options.updateWhenIdle) { 66 map.on('moveend', this._update, this); 67 } else { 68 map.on('move', this._update, this); 69 } 70 map.on('zoomend', this._handleZoomAnim, this); 71 map.on('resize', this._resize, this); 72 73 //handle layer being added to a map for which there are no Google tiles at the given zoom 74 google.maps.event.addListenerOnce(this._mutant, 'idle', function () { 75 this._checkZoomLevels(); 76 this._mutantIsReady = true; 77 }.bind(this)); 78 79 //20px instead of 1em to avoid a slight overlap with google's attribution 80 map._controlCorners.bottomright.style.marginBottom = '20px'; 81 map._controlCorners.bottomleft.style.marginBottom = '20px'; 82 83 this._reset(); 84 this._update(); 85 86 if (this._subLayers) { 87 //restore previously added google layers 88 for (var layerName in this._subLayers) { 89 this._subLayers[layerName].setMap(this._mutant); 90 } 91 } 92 }.bind(this)); 93 }, 94 95 onRemove: function (map) { 96 L.GridLayer.prototype.onRemove.call(this, map); 97 this._observer.disconnect(); 98 map._container.removeChild(this._mutantContainer); 99 100 google.maps.event.clearListeners(map, 'idle'); 101 google.maps.event.clearListeners(this._mutant, 'idle'); 102 map.off('viewreset', this._reset, this); 103 map.off('move', this._update, this); 104 map.off('moveend', this._update, this); 105 map.off('zoomend', this._handleZoomAnim, this); 106 map.off('resize', this._resize, this); 107 108 if (map._controlCorners) { 109 map._controlCorners.bottomright.style.marginBottom = '0em'; 110 map._controlCorners.bottomleft.style.marginBottom = '0em'; 111 } 112 }, 113 114 getAttribution: function () { 115 return this.options.attribution; 116 }, 117 118 setElementSize: function (e, size) { 119 e.style.width = size.x + 'px'; 120 e.style.height = size.y + 'px'; 121 }, 122 123 124 addGoogleLayer: function (googleLayerName, options) { 125 if (!this._subLayers) this._subLayers = {}; 126 return this._GAPIPromise.then(function () { 127 var Constructor = google.maps[googleLayerName]; 128 var googleLayer = new Constructor(options); 129 googleLayer.setMap(this._mutant); 130 this._subLayers[googleLayerName] = googleLayer; 131 return googleLayer; 132 }.bind(this)); 133 }, 134 135 removeGoogleLayer: function (googleLayerName) { 136 var googleLayer = this._subLayers && this._subLayers[googleLayerName]; 137 if (!googleLayer) return; 138 139 googleLayer.setMap(null); 140 delete this._subLayers[googleLayerName]; 141 }, 142 143 144 _initMutantContainer: function () { 145 if (!this._mutantContainer) { 146 this._mutantContainer = L.DomUtil.create('div', 'leaflet-google-mutant leaflet-top leaflet-left'); 147 this._mutantContainer.id = '_MutantContainer_' + L.Util.stamp(this._mutantContainer); 148 this._mutantContainer.style.zIndex = '800'; //leaflet map pane at 400, controls at 1000 149 this._mutantContainer.style.pointerEvents = 'none'; 150 151 L.DomEvent.off(this._mutantContainer); 152 153 } 154 this._map.getContainer().appendChild(this._mutantContainer); 155 156 this.setOpacity(this.options.opacity); 157 this.setElementSize(this._mutantContainer, this._map.getSize()); 158 159 this._attachObserver(this._mutantContainer); 160 }, 161 162 _initMutant: function () { 163 if (!this._ready || !this._mutantContainer) return; 164 165 if (this._mutant) { 166 // reuse old _mutant, just make sure it has the correct size 167 this._resize(); 168 return; 169 } 170 171 this._mutantCenter = new google.maps.LatLng(0, 0); 172 173 var map = new google.maps.Map(this._mutantContainer, { 174 center: this._mutantCenter, 175 zoom: 0, 176 tilt: 0, 177 mapTypeId: this.options.type, 178 disableDefaultUI: true, 179 keyboardShortcuts: false, 180 draggable: false, 181 disableDoubleClickZoom: true, 182 scrollwheel: false, 183 streetViewControl: false, 184 styles: this.options.styles || {}, 185 backgroundColor: 'transparent' 186 }); 187 188 this._mutant = map; 189 190 google.maps.event.addListenerOnce(map, 'idle', function () { 191 var nodes = this._mutantContainer.querySelectorAll('a'); 192 for (var i = 0; i < nodes.length; i++) { 193 nodes[i].style.pointerEvents = 'auto'; 194 } 195 }.bind(this)); 196 197 // ðevent spawned 198 // Fired when the mutant has been created. 199 this.fire('spawned', {mapObject: map}); 200 }, 201 202 _attachObserver: function _attachObserver (node) { 203// console.log('Gonna observe', node); 204 205 if (!this._observer) 206 this._observer = new MutationObserver(this._onMutations.bind(this)); 207 208 // pass in the target node, as well as the observer options 209 this._observer.observe(node, { childList: true, subtree: true }); 210 211 // if we are reusing an old _mutantContainer, we must manually detect 212 // all existing tiles in it 213 Array.prototype.forEach.call( 214 node.querySelectorAll('img'), 215 this._boundOnMutatedImage 216 ); 217 }, 218 219 _onMutations: function _onMutations (mutations) { 220 for (var i = 0; i < mutations.length; ++i) { 221 var mutation = mutations[i]; 222 for (var j = 0; j < mutation.addedNodes.length; ++j) { 223 var node = mutation.addedNodes[j]; 224 225 if (node instanceof HTMLImageElement) { 226 this._onMutatedImage(node); 227 } else if (node instanceof HTMLElement) { 228 Array.prototype.forEach.call( 229 node.querySelectorAll('img'), 230 this._boundOnMutatedImage 231 ); 232 233 // Check for, and remove, the "Google Maps can't load correctly" div. 234 // You *are* loading correctly, you dumbwit. 235 if (node.style.backgroundColor === 'white') { 236 L.DomUtil.remove(node); 237 } 238 239 // Check for, and remove, the "For development purposes only" divs on the aerial/hybrid tiles. 240 if (node.textContent.indexOf('For development purposes only') === 0) { 241 L.DomUtil.remove(node); 242 } 243 244 // Check for, and remove, the "Sorry, we have no imagery here" 245 // empty <div>s. The [style*="text-align: center"] selector 246 // avoids matching the attribution notice. 247 // This empty div doesn't have a reference to the tile 248 // coordinates, so it's not possible to mark the tile as 249 // failed. 250 Array.prototype.forEach.call( 251 node.querySelectorAll('div[draggable=false][style*="text-align: center"]'), 252 L.DomUtil.remove 253 ); 254 } 255 } 256 } 257 }, 258 259 // Only images which 'src' attrib match this will be considered for moving around. 260 // Looks like some kind of string-based protobuf, maybe?? 261 // Only the roads (and terrain, and vector-based stuff) match this pattern 262 _roadRegexp: /!1i(\d+)!2i(\d+)!3i(\d+)!/, 263 264 // On the other hand, raster imagery matches this other pattern 265 _satRegexp: /x=(\d+)&y=(\d+)&z=(\d+)/, 266 267 // On small viewports, when zooming in/out, a static image is requested 268 // This will not be moved around, just removed from the DOM. 269 _staticRegExp: /StaticMapService\.GetMapImage/, 270 271 _onMutatedImage: function _onMutatedImage (imgNode) { 272// if (imgNode.src) { 273// console.log('caught mutated image: ', imgNode.src); 274// } 275 276 var coords; 277 var match = imgNode.src.match(this._roadRegexp); 278 var sublayer = 0; 279 280 if (match) { 281 coords = { 282 z: match[1], 283 x: match[2], 284 y: match[3] 285 }; 286 if (this._imagesPerTile > 1) { 287 imgNode.style.zIndex = 1; 288 sublayer = 1; 289 } 290 } else { 291 match = imgNode.src.match(this._satRegexp); 292 if (match) { 293 coords = { 294 x: match[1], 295 y: match[2], 296 z: match[3] 297 }; 298 } 299// imgNode.style.zIndex = 0; 300 sublayer = 0; 301 } 302 303 if (coords) { 304 var tileKey = this._tileCoordsToKey(coords); 305 imgNode.style.position = 'absolute'; 306 imgNode.style.visibility = 'hidden'; 307 308 var key = tileKey + '/' + sublayer; 309 // console.log('mutation for tile', key) 310 //store img so it can also be used in subsequent tile requests 311 this._freshTiles[key] = imgNode; 312 313 if (key in this._tileCallbacks && this._tileCallbacks[key]) { 314// console.log('Fullfilling callback ', key); 315 //fullfill most recent tileCallback because there maybe callbacks that will never get a 316 //corresponding mutation (because map moved to quickly...) 317 this._tileCallbacks[key].pop()(imgNode); 318 if (!this._tileCallbacks[key].length) { delete this._tileCallbacks[key]; } 319 } else { 320 if (this._tiles[tileKey]) { 321 //we already have a tile in this position (mutation is probably a google layer being added) 322 //replace it 323 var c = this._tiles[tileKey].el; 324 var oldImg = (sublayer === 0) ? c.firstChild : c.firstChild.nextSibling; 325 var cloneImgNode = this._clone(imgNode); 326 c.replaceChild(cloneImgNode, oldImg); 327 } 328 } 329 } else if (imgNode.src.match(this._staticRegExp)) { 330 imgNode.style.visibility = 'hidden'; 331 } 332 }, 333 334 335 createTile: function (coords, done) { 336 var key = this._tileCoordsToKey(coords); 337 338 var tileContainer = L.DomUtil.create('div'); 339 tileContainer.dataset.pending = this._imagesPerTile; 340 done = done.bind(this, null, tileContainer); 341 342 for (var i = 0; i < this._imagesPerTile; i++) { 343 var key2 = key + '/' + i; 344 if (key2 in this._freshTiles) { 345 var imgNode = this._freshTiles[key2]; 346 tileContainer.appendChild(this._clone(imgNode)); 347 tileContainer.dataset.pending--; 348// console.log('Got ', key2, ' from _freshTiles'); 349 } else { 350 this._tileCallbacks[key2] = this._tileCallbacks[key2] || []; 351 this._tileCallbacks[key2].push( (function (c/*, k2*/) { 352 return function (imgNode) { 353 c.appendChild(this._clone(imgNode)); 354 c.dataset.pending--; 355 if (!parseInt(c.dataset.pending)) { done(); } 356// console.log('Sent ', k2, ' to _tileCallbacks, still ', c.dataset.pending, ' images to go'); 357 }.bind(this); 358 }.bind(this))(tileContainer/*, key2*/) ); 359 } 360 } 361 362 if (!parseInt(tileContainer.dataset.pending)) {
363 L.Util.requestAnimFrame(done); 364 } 365 return tileContainer; 366 }, 367 368 _clone: function (imgNode) { 369 var clonedImgNode = imgNode.cloneNode(true); 370 clonedImgNode.style.visibility = 'visible'; 371 return clonedImgNode; 372 }, 373 374 _checkZoomLevels: function () { 375 //setting the zoom level on the Google map may result in a different zoom level than the one requested 376 //(it won't go beyond the level for which they have data). 377 var zoomLevel = this._map.getZoom(); 378 var gMapZoomLevel = this._mutant.getZoom(); 379 if (!zoomLevel || !gMapZoomLevel) return; 380 381 382 if ((gMapZoomLevel !== zoomLevel) || //zoom levels are out of sync, Google doesn't have data 383 (gMapZoomLevel > this.options.maxNativeZoom)) { //at current location, Google does have data (contrary to maxNativeZoom) 384 //Update maxNativeZoom 385 this._setMaxNativeZoom(gMapZoomLevel); 386 } 387 }, 388 389 _setMaxNativeZoom: function (zoomLevel) { 390 if (zoomLevel != this.options.maxNativeZoom) { 391 this.options.maxNativeZoom = zoomLevel; 392 this._resetView(); 393 } 394 }, 395 396 _reset: function () { 397 this._initContainer(); 398 }, 399 400 _update: function () { 401 // zoom level check needs to happen before super's implementation (tile addition/creation) 402 // otherwise tiles may be missed if maxNativeZoom is not yet correctly determined 403 if (this._mutant) { 404 var center = this._map.getCenter(); 405 var _center = new google.maps.LatLng(center.lat, center.lng); 406 407 this._mutant.setCenter(_center); 408 var zoom = this._map.getZoom(); 409 var fractionalLevel = zoom !== Math.round(zoom); 410 var mutantZoom = this._mutant.getZoom(); 411 412 //ignore fractional zoom levels 413 if (!fractionalLevel && (zoom != mutantZoom)) { 414 this._mutant.setZoom(zoom); 415 416 if (this._mutantIsReady) this._checkZoomLevels(); 417 //else zoom level check will be done later by 'idle' handler 418 } 419 } 420 421 L.GridLayer.prototype._update.call(this); 422 }, 423 424 _resize: function () { 425 var size = this._map.getSize(); 426 if (this._mutantContainer.style.width === size.x && 427 this._mutantContainer.style.height === size.y) 428 return; 429 this.setElementSize(this._mutantContainer, size); 430 if (!this._mutant) return; 431 google.maps.event.trigger(this._mutant, 'resize'); 432 }, 433 434 _handleZoomAnim: function () { 435 if (!this._mutant) return; 436 var center = this._map.getCenter(); 437 var _center = new google.maps.LatLng(center.lat, center.lng); 438 439 this._mutant.setCenter(_center); 440 this._mutant.setZoom(Math.round(this._map.getZoom())); 441 }, 442 443 // Agressively prune _freshtiles when a tile with the same key is removed, 444 // this prevents a problem where Leaflet keeps a loaded tile longer than 445 // GMaps, so that GMaps makes two requests but Leaflet only consumes one, 446 // polluting _freshTiles with stale data. 447 _removeTile: function (key) { 448 if (!this._mutant) return; 449 450 //give time for animations to finish before checking it tile should be pruned 451 setTimeout(this._pruneTile.bind(this, key), 1000); 452 453 454 return L.GridLayer.prototype._removeTile.call(this, key); 455 }, 456 457 _pruneTile: function (key) { 458 var gZoom = this._mutant.getZoom(); 459 var tileZoom = key.split(':')[2]; 460 var googleBounds = this._mutant.getBounds();
461 var sw = googleBounds.getSouthWest(); 462 var ne = googleBounds.getNorthEast(); 463 var gMapBounds = L.latLngBounds([[sw.lat(), sw.lng()], [ne.lat(), ne.lng()]]); 464 465 for (var i=0; i<this._imagesPerTile; i++) { 466 var key2 = key + '/' + i; 467 if (key2 in this._freshTiles) { 468 var tileBounds = this._map && this._keyToBounds(key); 469 var stillVisible = this._map && tileBounds.overlaps(gMapBounds) && (tileZoom == gZoom); 470 471 if (!stillVisible) delete this._freshTiles[key2]; 472// console.log('Prunning of ', key, (!stillVisible)) 473 } 474 } 475 } 476}); 477 478 479// ðfactory gridLayer.googleMutant(options) 480// Returns a new `GridLayer.GoogleMutant` given its options 481L.gridLayer.googleMutant = function (options) { 482 return new L.GridLayer.GoogleMutant(options); 483};
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.