[PLUGINS] ~gis v4.41.1 --> v4.43.1
[lhc/web/www.git] / www / plugins / gis / lib / leaflet / plugins / Leaflet.GoogleMutant.js
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
7 L.GridLayer.GoogleMutant = L.GridLayer.extend({
8 includes: L.Mixin.Events,
9
10 options: {
11 minZoom: 0,
12 maxZoom: 18,
13 tileSize: 256,
14 subdomains: 'abc',
15 errorTileUrl: '',
16 attribution: '', // The mutant container will add its own attribution anyways.
17 opacity: 1,
18 continuousWorld: false,
19 noWrap: false,
20 // 🍂option type: String = 'roadmap'
21 // Google's map type. Valid values are 'roadmap', 'satellite' or 'terrain'. 'hybrid' is not really supported.
22 type: 'roadmap',
23 maxNativeZoom: 21
24 },
25
26 initialize: function (options) {
27 L.GridLayer.prototype.initialize.call(this, options);
28
29 this._ready = !!window.google && !!window.google.maps && !!window.google.maps.Map;
30
31 this._GAPIPromise = this._ready ? Promise.resolve(window.google) : new Promise(function (resolve, reject) {
32 var checkCounter = 0;
33 var intervalId = null;
34 intervalId = setInterval(function () {
35 if (checkCounter >= 10) {
36 clearInterval(intervalId);
37 return reject(new Error('window.google not found after 10 attempts'));
38 }
39 if (!!window.google && !!window.google.maps && !!window.google.maps.Map) {
40 clearInterval(intervalId);
41 return resolve(window.google);
42 }
43 checkCounter++;
44 }, 500);
45 });
46
47 // Couple data structures indexed by tile key
48 this._tileCallbacks = {}; // Callbacks for promises for tiles that are expected
49 this._freshTiles = {}; // Tiles from the mutant which haven't been requested yet
50
51 this._imagesPerTile = (this.options.type === 'hybrid') ? 2 : 1;
52 this.createTile = (this.options.type === 'hybrid') ? this._createMultiTile : this._createSingleTile;
53 },
54
55 onAdd: function (map) {
56 L.GridLayer.prototype.onAdd.call(this, map);
57 this._initMutantContainer();
58
59 this._GAPIPromise.then(function () {
60 this._ready = true;
61 this._map = map;
62
63 this._initMutant();
64
65 map.on('viewreset', this._reset, this);
66 map.on('move', this._update, this);
67 map.on('zoomend', this._handleZoomAnim, this);
68 map.on('resize', this._resize, this);
69
70 //20px instead of 1em to avoid a slight overlap with google's attribution
71 map._controlCorners.bottomright.style.marginBottom = '20px';
72
73 this._reset();
74 this._update();
75 }.bind(this));
76 },
77
78 onRemove: function (map) {
79 L.GridLayer.prototype.onRemove.call(this, map);
80 map._container.removeChild(this._mutantContainer);
81 this._mutantContainer = undefined;
82
83 map.off('viewreset', this._reset, this);
84 map.off('move', this._update, this);
85 map.off('zoomend', this._handleZoomAnim, this);
86 map.off('resize', this._resize, this);
87
88 map._controlCorners.bottomright.style.marginBottom = '0em';
89 },
90
91 getAttribution: function () {
92 return this.options.attribution;
93 },
94
95 setOpacity: function (opacity) {
96 this.options.opacity = opacity;
97 if (opacity < 1) {
98 L.DomUtil.setOpacity(this._mutantContainer, opacity);
99 }
100 },
101
102 setElementSize: function (e, size) {
103 e.style.width = size.x + 'px';
104 e.style.height = size.y + 'px';
105 },
106
107 _initMutantContainer: function () {
108 if (!this._mutantContainer) {
109 this._mutantContainer = L.DomUtil.create('div', 'leaflet-google-mutant leaflet-top leaflet-left');
110 this._mutantContainer.id = '_MutantContainer_' + L.Util.stamp(this._mutantContainer);
111 // this._mutantContainer.style.zIndex = 'auto';
112 this._mutantContainer.style.pointerEvents = 'none';
113
114 this._map.getContainer().appendChild(this._mutantContainer);
115 }
116
117 this.setOpacity(this.options.opacity);
118 this.setElementSize(this._mutantContainer, this._map.getSize());
119
120 this._attachObserver(this._mutantContainer);
121 },
122
123 _initMutant: function () {
124 if (!this._ready || !this._mutantContainer) return;
125 this._mutantCenter = new google.maps.LatLng(0, 0);
126
127 var map = new google.maps.Map(this._mutantContainer, {
128 center: this._mutantCenter,
129 zoom: 0,
130 tilt: 0,
131 mapTypeId: this.options.type,
132 disableDefaultUI: true,
133 keyboardShortcuts: false,
134 draggable: false,
135 disableDoubleClickZoom: true,
136 scrollwheel: false,
137 streetViewControl: false,
138 styles: this.options.styles || {},
139 backgroundColor: 'transparent'
140 });
141
142 this._mutant = map;
143
144 // 🍂event spawned
145 // Fired when the mutant has been created.
146 this.fire('spawned', {mapObject: map});
147 },
148
149 _attachObserver: function _attachObserver (node) {
150 // console.log('Gonna observe', node);
151
152 var observer = new MutationObserver(this._onMutations.bind(this));
153
154 // pass in the target node, as well as the observer options
155 observer.observe(node, { childList: true, subtree: true });
156 },
157
158 _onMutations: function _onMutations (mutations) {
159 for (var i = 0; i < mutations.length; ++i) {
160 var mutation = mutations[i];
161 for (var j = 0; j < mutation.addedNodes.length; ++j) {
162 var node = mutation.addedNodes[j];
163
164 if (node instanceof HTMLImageElement) {
165 this._onMutatedImage(node);
166 } else if (node instanceof HTMLElement) {
167 Array.prototype.forEach.call(node.querySelectorAll('img'), this._onMutatedImage.bind(this));
168 }
169 }
170 }
171 },
172
173 // Only images which 'src' attrib match this will be considered for moving around.
174 // Looks like some kind of string-based protobuf, maybe??
175 // Only the roads (and terrain, and vector-based stuff) match this pattern
176 _roadRegexp: /!1i(\d+)!2i(\d+)!3i(\d+)!/,
177
178 // On the other hand, raster imagery matches this other pattern
179 _satRegexp: /x=(\d+)&y=(\d+)&z=(\d+)/,
180
181 // On small viewports, when zooming in/out, a static image is requested
182 // This will not be moved around, just removed from the DOM.
183 _staticRegExp: /StaticMapService\.GetMapImage/,
184
185 _onMutatedImage: function _onMutatedImage (imgNode) {
186 // if (imgNode.src) {
187 // console.log('caught mutated image: ', imgNode.src);
188 // }
189
190 var coords;
191 var match = imgNode.src.match(this._roadRegexp);
192 var sublayer, parent;
193
194 if (match) {
195 coords = {
196 z: match[1],
197 x: match[2],
198 y: match[3]
199 };
200 if (this._imagesPerTile > 1) { imgNode.style.zIndex = 1; }
201 sublayer = 1;
202 } else {
203 match = imgNode.src.match(this._satRegexp);
204 if (match) {
205 coords = {
206 x: match[1],
207 y: match[2],
208 z: match[3]
209 };
210 }
211 // imgNode.style.zIndex = 0;
212 sublayer = 0;
213 }
214
215 if (coords) {
216 var key = this._tileCoordsToKey(coords);
217 if (this._imagesPerTile > 1) { key += '/' + sublayer; }
218 if (key in this._tileCallbacks && this._tileCallbacks[key]) {
219 // console.log('Fullfilling callback ', key);
220 this._tileCallbacks[key].pop()(imgNode);
221 if (!this._tileCallbacks[key].length) { delete this._tileCallbacks[key]; }
222 } else {
223 // console.log('Caching for later', key);
224 parent = imgNode.parentNode;
225 if (parent) {
226 parent.removeChild(imgNode);
227 parent.removeChild = L.Util.falseFn;
228 // imgNode.parentNode.replaceChild(L.DomUtil.create('img'), imgNode);
229 }
230 if (key in this._freshTiles) {
231 this._freshTiles[key].push(imgNode);
232 } else {
233 this._freshTiles[key] = [imgNode];
234 }
235 }
236 } else if (imgNode.src.match(this._staticRegExp)) {
237 parent = imgNode.parentNode;
238 if (parent) {
239 // Remove the image, but don't store it anywhere.
240 // Image needs to be replaced instead of removed, as the container
241 // seems to be reused.
242 imgNode.parentNode.replaceChild(L.DomUtil.create('img'), imgNode);
243 }
244 }
245 },
246
247 // This will be used as this.createTile for 'roadmap', 'sat', 'terrain'
248 _createSingleTile: function createTile (coords, done) {
249 var key = this._tileCoordsToKey(coords);
250 // console.log('Need:', key);
251
252 if (key in this._freshTiles) {
253 var tile = this._freshTiles[key].pop();
254 if (!this._freshTiles[key].length) { delete this._freshTiles[key]; }
255 L.Util.requestAnimFrame(done);
256 // console.log('Got ', key, ' from _freshTiles');
257 return tile;
258 } else {
259 var tileContainer = L.DomUtil.create('div');
260 this._tileCallbacks[key] = this._tileCallbacks[key] || [];
261 this._tileCallbacks[key].push( (function (c/*, k*/) {
262 return function (imgNode) {
263 var parent = imgNode.parentNode;
264 if (parent) {
265 parent.removeChild(imgNode);
266 parent.removeChild = L.Util.falseFn;
267 // imgNode.parentNode.replaceChild(L.DomUtil.create('img'), imgNode);
268 }
269 c.appendChild(imgNode);
270 done();
271 // console.log('Sent ', k, ' to _tileCallbacks');
272 }.bind(this);
273 }.bind(this))(tileContainer/*, key*/) );
274
275 return tileContainer;
276 }
277 },
278
279 // This will be used as this.createTile for 'hybrid'
280 _createMultiTile: function createTile (coords, done) {
281 var key = this._tileCoordsToKey(coords);
282
283 var tileContainer = L.DomUtil.create('div');
284 tileContainer.dataset.pending = this._imagesPerTile;
285
286 for (var i = 0; i < this._imagesPerTile; i++) {
287 var key2 = key + '/' + i;
288 if (key2 in this._freshTiles) {
289 tileContainer.appendChild(this._freshTiles[key2].pop());
290 if (!this._freshTiles[key2].length) { delete this._freshTiles[key2]; }
291 tileContainer.dataset.pending--;
292 // console.log('Got ', key2, ' from _freshTiles');
293 } else {
294 this._tileCallbacks[key2] = this._tileCallbacks[key2] || [];
295 this._tileCallbacks[key2].push( (function (c/*, k2*/) {
296 return function (imgNode) {
297 var parent = imgNode.parentNode;
298 if (parent) {
299 parent.removeChild(imgNode);
300 parent.removeChild = L.Util.falseFn;
301 // imgNode.parentNode.replaceChild(L.DomUtil.create('img'), imgNode);
302 }
303 c.appendChild(imgNode);
304 c.dataset.pending--;
305 if (!parseInt(c.dataset.pending)) { done(); }
306 // console.log('Sent ', k2, ' to _tileCallbacks, still ', c.dataset.pending, ' images to go');
307 }.bind(this);
308 }.bind(this))(tileContainer/*, key2*/) );
309 }
310 }
311
312 if (!parseInt(tileContainer.dataset.pending)) {
313 L.Util.requestAnimFrame(done);
314 }
315 return tileContainer;
316 },
317
318 _checkZoomLevels: function () {
319 //setting the zoom level on the Google map may result in a different zoom level than the one requested
320 //(it won't go beyond the level for which they have data).
321 // verify and make sure the zoom levels on both Leaflet and Google maps are consistent
322 if ((this._map.getZoom() !== undefined) && (this._mutant.getZoom() !== this._map.getZoom())) {
323 //zoom levels are out of sync. Set the leaflet zoom level to match the google one
324 this._map.setZoom(this._mutant.getZoom());
325 }
326 },
327
328 _reset: function () {
329 this._initContainer();
330 },
331
332 _update: function () {
333 L.GridLayer.prototype._update.call(this);
334 if (!this._mutant) return;
335
336 var center = this._map.getCenter();
337 var _center = new google.maps.LatLng(center.lat, center.lng);
338
339 this._mutant.setCenter(_center);
340 var zoom = this._map.getZoom();
341 if (zoom !== undefined) {
342 this._mutant.setZoom(Math.round(this._map.getZoom()));
343 }
344 },
345
346 _resize: function () {
347 var size = this._map.getSize();
348 if (this._mutantContainer.style.width === size.x &&
349 this._mutantContainer.style.height === size.y)
350 return;
351 this.setElementSize(this._mutantContainer, size);
352 if (!this._mutant) return;
353 google.maps.event.trigger(this._mutant, 'resize');
354 },
355
356 _handleZoomAnim: function () {
357 var center = this._map.getCenter();
358 var _center = new google.maps.LatLng(center.lat, center.lng);
359
360 this._mutant.setCenter(_center);
361 this._mutant.setZoom(Math.round(this._map.getZoom()));
362 },
363
364 // Agressively prune _freshtiles when a tile with the same key is removed,
365 // this prevents a problem where Leaflet keeps a loaded tile longer than
366 // GMaps, so that GMaps makes two requests but Leaflet only consumes one,
367 // polluting _freshTiles with stale data.
368 _removeTile: function (key) {
369 if (this._imagesPerTile > 1) {
370 for (var i=0; i<this._imagesPerTile; i++) {
371 var key2 = key + '/' + i;
372 if (key2 in this._freshTiles) { delete this._freshTiles[key2]; }
373 // console.log('Pruned spurious hybrid _freshTiles');
374 }
375 } else {
376 if (key in this._freshTiles) {
377 delete this._freshTiles[key];
378 // console.log('Pruned spurious _freshTiles', key);
379 }
380 }
381
382 return L.GridLayer.prototype._removeTile.call(this, key);
383 }
384 });
385
386
387 // 🍂factory gridLayer.googleMutant(options)
388 // Returns a new `GridLayer.GoogleMutant` given its options
389 L.gridLayer.googleMutant = function (options) {
390 return new L.GridLayer.GoogleMutant(options);
391 };