--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_gis/resources/js/leaflet-google-mutant.js Thu May 18 17:23:48 2017 +0200
@@ -0,0 +1,439 @@
+// Based on https://github.com/shramov/leaflet-plugins
+// GridLayer like https://avinmathew.com/leaflet-and-google-maps/ , but using MutationObserver instead of jQuery
+
+
+// 🍂class GridLayer.GoogleMutant
+// 🍂extends GridLayer
+L.GridLayer.GoogleMutant = L.GridLayer.extend({
+ includes: L.Mixin.Events,
+
+ options: {
+ minZoom: 0,
+ maxZoom: 18,
+ tileSize: 256,
+ subdomains: 'abc',
+ errorTileUrl: '',
+ attribution: '', // The mutant container will add its own attribution anyways.
+ opacity: 1,
+ continuousWorld: false,
+ noWrap: false,
+ // 🍂option type: String = 'roadmap'
+ // Google's map type. Valid values are 'roadmap', 'satellite' or 'terrain'. 'hybrid' is not really supported.
+ type: 'roadmap',
+ maxNativeZoom: 21
+ },
+
+ initialize: function (options) {
+ L.GridLayer.prototype.initialize.call(this, options);
+
+ this._ready = !!window.google && !!window.google.maps && !!window.google.maps.Map;
+
+ this._GAPIPromise = this._ready ? Promise.resolve(window.google) : new Promise(function (resolve, reject) {
+ var checkCounter = 0;
+ var intervalId = null;
+ intervalId = setInterval(function () {
+ if (checkCounter >= 10) {
+ clearInterval(intervalId);
+ return reject(new Error('window.google not found after 10 attempts'));
+ }
+ if (!!window.google && !!window.google.maps && !!window.google.maps.Map) {
+ clearInterval(intervalId);
+ return resolve(window.google);
+ }
+ checkCounter++;
+ }, 500);
+ });
+
+ // Couple data structures indexed by tile key
+ this._tileCallbacks = {}; // Callbacks for promises for tiles that are expected
+ this._freshTiles = {}; // Tiles from the mutant which haven't been requested yet
+
+ this._imagesPerTile = (this.options.type === 'hybrid') ? 2 : 1;
+ },
+
+ onAdd: function (map) {
+ L.GridLayer.prototype.onAdd.call(this, map);
+ this._initMutantContainer();
+
+ this._GAPIPromise.then(function () {
+ this._ready = true;
+ this._map = map;
+
+ this._initMutant();
+
+ map.on('viewreset', this._reset, this);
+ map.on('move', this._update, this);
+ map.on('zoomend', this._handleZoomAnim, this);
+ map.on('resize', this._resize, this);
+
+ //handle layer being added to a map for which there are no Google tiles at the given zoom
+ google.maps.event.addListenerOnce(this._mutant, 'idle', function () {
+ this._checkZoomLevels();
+ this._mutantIsReady = true;
+ }.bind(this));
+
+ //20px instead of 1em to avoid a slight overlap with google's attribution
+ map._controlCorners.bottomright.style.marginBottom = '20px';
+ map._controlCorners.bottomleft.style.marginBottom = '20px';
+
+ this._reset();
+ this._update();
+
+ if (this._subLayers) {
+ //restore previously added google layers
+ for (var layerName in this._subLayers) {
+ this._subLayers[layerName].setMap(this._mutant);
+ }
+ }
+ }.bind(this));
+ },
+
+ onRemove: function (map) {
+ L.GridLayer.prototype.onRemove.call(this, map);
+ map._container.removeChild(this._mutantContainer);
+ this._mutantContainer = undefined;
+
+ map.off('viewreset', this._reset, this);
+ map.off('move', this._update, this);
+ map.off('zoomend', this._handleZoomAnim, this);
+ map.off('resize', this._resize, this);
+
+ map._controlCorners.bottomright.style.marginBottom = '0em';
+ map._controlCorners.bottomleft.style.marginBottom = '0em';
+ },
+
+ getAttribution: function () {
+ return this.options.attribution;
+ },
+
+ setOpacity: function (opacity) {
+ this.options.opacity = opacity;
+ if (opacity < 1) {
+ L.DomUtil.setOpacity(this._mutantContainer, opacity);
+ }
+ },
+
+ setElementSize: function (e, size) {
+ e.style.width = size.x + 'px';
+ e.style.height = size.y + 'px';
+ },
+
+
+ addGoogleLayer: function (googleLayerName, options) {
+ if (!this._subLayers) this._subLayers = {};
+ return this._GAPIPromise.then(function () {
+ var Constructor = google.maps[googleLayerName];
+ var googleLayer = new Constructor(options);
+ googleLayer.setMap(this._mutant);
+ this._subLayers[googleLayerName] = googleLayer;
+ return googleLayer;
+ }.bind(this));
+ },
+
+ removeGoogleLayer: function (googleLayerName) {
+ var googleLayer = this._subLayers && this._subLayers[googleLayerName];
+ if (!googleLayer) return;
+
+ googleLayer.setMap(null);
+ delete this._subLayers[googleLayerName];
+ },
+
+
+ _initMutantContainer: function () {
+ if (!this._mutantContainer) {
+ this._mutantContainer = L.DomUtil.create('div', 'leaflet-google-mutant leaflet-top leaflet-left');
+ this._mutantContainer.id = '_MutantContainer_' + L.Util.stamp(this._mutantContainer);
+ this._mutantContainer.style.zIndex = '800'; //leaflet map pane at 400, controls at 1000
+ this._mutantContainer.style.pointerEvents = 'none';
+
+ this._map.getContainer().appendChild(this._mutantContainer);
+ }
+
+ this.setOpacity(this.options.opacity);
+ this.setElementSize(this._mutantContainer, this._map.getSize());
+
+ this._attachObserver(this._mutantContainer);
+ },
+
+ _initMutant: function () {
+ if (!this._ready || !this._mutantContainer) return;
+ this._mutantCenter = new google.maps.LatLng(0, 0);
+
+ var map = new google.maps.Map(this._mutantContainer, {
+ center: this._mutantCenter,
+ zoom: 0,
+ tilt: 0,
+ mapTypeId: this.options.type,
+ disableDefaultUI: true,
+ keyboardShortcuts: false,
+ draggable: false,
+ disableDoubleClickZoom: true,
+ scrollwheel: false,
+ streetViewControl: false,
+ styles: this.options.styles || {},
+ backgroundColor: 'transparent'
+ });
+
+ this._mutant = map;
+
+ google.maps.event.addListenerOnce(map, 'idle', function () {
+ var nodes = this._mutantContainer.querySelectorAll('a');
+ for (var i = 0; i < nodes.length; i++) {
+ nodes[i].style.pointerEvents = 'auto';
+ }
+ }.bind(this));
+
+ // 🍂event spawned
+ // Fired when the mutant has been created.
+ this.fire('spawned', {mapObject: map});
+ },
+
+ _attachObserver: function _attachObserver (node) {
+// console.log('Gonna observe', node);
+
+ var observer = new MutationObserver(this._onMutations.bind(this));
+
+ // pass in the target node, as well as the observer options
+ observer.observe(node, { childList: true, subtree: true });
+ },
+
+ _onMutations: function _onMutations (mutations) {
+ for (var i = 0; i < mutations.length; ++i) {
+ var mutation = mutations[i];
+ for (var j = 0; j < mutation.addedNodes.length; ++j) {
+ var node = mutation.addedNodes[j];
+
+ if (node instanceof HTMLImageElement) {
+ this._onMutatedImage(node);
+ } else if (node instanceof HTMLElement) {
+ Array.prototype.forEach.call(node.querySelectorAll('img'), this._onMutatedImage.bind(this));
+ }
+ }
+ }
+ },
+
+ // Only images which 'src' attrib match this will be considered for moving around.
+ // Looks like some kind of string-based protobuf, maybe??
+ // Only the roads (and terrain, and vector-based stuff) match this pattern
+ _roadRegexp: /!1i(\d+)!2i(\d+)!3i(\d+)!/,
+
+ // On the other hand, raster imagery matches this other pattern
+ _satRegexp: /x=(\d+)&y=(\d+)&z=(\d+)/,
+
+ // On small viewports, when zooming in/out, a static image is requested
+ // This will not be moved around, just removed from the DOM.
+ _staticRegExp: /StaticMapService\.GetMapImage/,
+
+ _onMutatedImage: function _onMutatedImage (imgNode) {
+// if (imgNode.src) {
+// console.log('caught mutated image: ', imgNode.src);
+// }
+
+ var coords;
+ var match = imgNode.src.match(this._roadRegexp);
+ var sublayer = 0;
+
+ if (match) {
+ coords = {
+ z: match[1],
+ x: match[2],
+ y: match[3]
+ };
+ if (this._imagesPerTile > 1) {
+ imgNode.style.zIndex = 1;
+ sublayer = 1;
+ }
+ } else {
+ match = imgNode.src.match(this._satRegexp);
+ if (match) {
+ coords = {
+ x: match[1],
+ y: match[2],
+ z: match[3]
+ };
+ }
+// imgNode.style.zIndex = 0;
+ sublayer = 0;
+ }
+
+ if (coords) {
+ var tileKey = this._tileCoordsToKey(coords);
+ imgNode.style.position = 'absolute';
+ var cloneImgNode = imgNode.cloneNode(true);
+ cloneImgNode.style.visibility = 'visible';
+ imgNode.style.visibility = 'hidden';
+
+ var key = tileKey + '/' + sublayer;
+ if (key in this._tileCallbacks && this._tileCallbacks[key]) {
+// console.log('Fullfilling callback ', key);
+ this._tileCallbacks[key].shift()(cloneImgNode);
+ if (!this._tileCallbacks[key].length) { delete this._tileCallbacks[key]; }
+ } else {
+// console.log('Caching for later', key);
+
+ if (this._tiles[tileKey]) {
+ //we already have a tile in this position (mutation is probably a google layer being added)
+ //replace it
+ var c = this._tiles[tileKey].el;
+ var oldImg = (sublayer === 0) ? c.firstChild : c.firstChild.nextSibling;
+ c.replaceChild(cloneImgNode, oldImg);
+ } else if (key in this._freshTiles) {
+ this._freshTiles[key].push(cloneImgNode);
+ } else {
+ this._freshTiles[key] = [cloneImgNode];
+ }
+ }
+ } else if (imgNode.src.match(this._staticRegExp)) {
+ imgNode.style.visibility = 'hidden';
+ }
+ },
+
+
+ createTile: function (coords, done) {
+ var key = this._tileCoordsToKey(coords);
+
+ var tileContainer = L.DomUtil.create('div');
+ tileContainer.dataset.pending = this._imagesPerTile;
+ done = done.bind(this, null, tileContainer);
+
+ for (var i = 0; i < this._imagesPerTile; i++) {
+ var key2 = key + '/' + i;
+ if (key2 in this._freshTiles) {
+ tileContainer.appendChild(this._freshTiles[key2].pop());
+ if (!this._freshTiles[key2].length) { delete this._freshTiles[key2]; }
+ tileContainer.dataset.pending--;
+// console.log('Got ', key2, ' from _freshTiles');
+ } else {
+ this._tileCallbacks[key2] = this._tileCallbacks[key2] || [];
+ this._tileCallbacks[key2].push( (function (c/*, k2*/) {
+ return function (cloneImgNode) {
+ c.appendChild(cloneImgNode);
+ c.dataset.pending--;
+ if (!parseInt(c.dataset.pending)) { done(); }
+// console.log('Sent ', k2, ' to _tileCallbacks, still ', c.dataset.pending, ' images to go');
+ }.bind(this);
+ }.bind(this))(tileContainer/*, key2*/) );
+ }
+ }
+
+ if (!parseInt(tileContainer.dataset.pending)) {
+ L.Util.requestAnimFrame(done);
+ }
+ return tileContainer;
+ },
+
+ _checkZoomLevels: function () {
+ //setting the zoom level on the Google map may result in a different zoom level than the one requested
+ //(it won't go beyond the level for which they have data).
+ var zoomLevel = this._map.getZoom();
+ var gMapZoomLevel = this._mutant.getZoom();
+ if (!zoomLevel || !gMapZoomLevel) return;
+
+
+ if ((gMapZoomLevel !== zoomLevel) || //zoom levels are out of sync, Google doesn't have data
+ (gMapZoomLevel > this.options.maxNativeZoom)) { //at current location, Google does have data (contrary to maxNativeZoom)
+ //Update maxNativeZoom
+ this._setMaxNativeZoom(gMapZoomLevel);
+ }
+ },
+
+ _setMaxNativeZoom: function (zoomLevel) {
+ if (zoomLevel != this.options.maxNativeZoom) {
+ this.options.maxNativeZoom = zoomLevel;
+ this._resetView();
+ }
+ },
+
+ _reset: function () {
+ this._initContainer();
+ },
+
+ _update: function () {
+ // zoom level check needs to happen before super's implementation (tile addition/creation)
+ // otherwise tiles may be missed if maxNativeZoom is not yet correctly determined
+ if (this._mutant) {
+ var center = this._map.getCenter();
+ var _center = new google.maps.LatLng(center.lat, center.lng);
+
+ this._mutant.setCenter(_center);
+ var zoom = this._map.getZoom();
+ var fractionalLevel = zoom !== Math.round(zoom);
+ var mutantZoom = this._mutant.getZoom();
+
+ //ignore fractional zoom levels
+ if (!fractionalLevel && (zoom != mutantZoom)) {
+ this._mutant.setZoom(zoom);
+
+ if (this._mutantIsReady) this._checkZoomLevels();
+ //else zoom level check will be done later by 'idle' handler
+ }
+ }
+
+ L.GridLayer.prototype._update.call(this);
+ },
+
+ _resize: function () {
+ var size = this._map.getSize();
+ if (this._mutantContainer.style.width === size.x &&
+ this._mutantContainer.style.height === size.y)
+ return;
+ this.setElementSize(this._mutantContainer, size);
+ if (!this._mutant) return;
+ google.maps.event.trigger(this._mutant, 'resize');
+ },
+
+ _handleZoomAnim: function () {
+ if (!this._mutant) return;
+ var center = this._map.getCenter();
+ var _center = new google.maps.LatLng(center.lat, center.lng);
+
+ this._mutant.setCenter(_center);
+ this._mutant.setZoom(Math.round(this._map.getZoom()));
+ },
+
+ // Agressively prune _freshtiles when a tile with the same key is removed,
+ // this prevents a problem where Leaflet keeps a loaded tile longer than
+ // GMaps, so that GMaps makes two requests but Leaflet only consumes one,
+ // polluting _freshTiles with stale data.
+ _removeTile: function (key) {
+ if (!this._mutant) return;
+
+ for (var i=0; i<this._imagesPerTile; i++) {
+ var key2 = key + '/' + i;
+ if (key2 in this._freshTiles) { delete this._freshTiles[key2]; }
+// console.log('Pruned spurious hybrid _freshTiles');
+ }
+
+ //if the tile is still visible in the google map, keep it.
+ //In this situation, if the tile is later required, there won't be a mutation event (since tile is already in gMap)
+ //and there will be no other way to refetch the tile.
+ //this situation where GMaps keeps a tile longer than Leaflet can happen when the map goes past
+ //self's maxNativeZoom
+ var gZoom = this._mutant.getZoom();
+ var zoom = key.split(':')[2];
+ if (zoom == gZoom && gZoom == this.options.maxNativeZoom) {
+ var imgs = this._tiles[key].el.querySelectorAll('img');
+ if (imgs.length) {
+ for (var j=0; j<this._imagesPerTile;j++) {
+ var keyJ = key + '/' + j;
+ var imgNode = imgs[j];
+ if (keyJ in this._freshTiles) {
+ this._freshTiles[keyJ].push(imgNode);
+ } else {
+ this._freshTiles[keyJ] = [imgNode];
+ }
+ }
+ }
+ }
+
+
+ return L.GridLayer.prototype._removeTile.call(this, key);
+ }
+});
+
+
+// 🍂factory gridLayer.googleMutant(options)
+// Returns a new `GridLayer.GoogleMutant` given its options
+L.gridLayer.googleMutant = function (options) {
+ return new L.GridLayer.GoogleMutant(options);
+};