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