PageSourceSearch

https://skyrealestate.co/wp-content/plugins/wp-realestate/assets/js/leaflet/Leaflet.GoogleMutant.js?ver=1.5.1

js skyrealestate.co collected 2026-10-04 13:25:30 UTC 15,328 bytes, 483 lines download raw bytes

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.