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