PageSourceSearch

https://www.cubelles.cat/front/view/js/bower_components/Leaflet/default/Leaflet.GoogleMutant.js

js cubelles.cat collected 2026-10-02 10:38:27 UTC 18,583 bytes, 468 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    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.