/* * L.Control.Loading is a control that shows a loading indicator when tiles are * loading or when map-related AJAX requests are taking place. */ (function () { var console = window.console || { error: function () {}, warn: function () {} }; function defineLeafletLoading(L) { L.Control.Loading = L.Control.extend({ options: { delayIndicator: null, position: 'topleft', separate: false, zoomControl: null, spinjs: false, spin: { lines: 7, length: 3, width: 3, radius: 5, rotate: 13, top: "83%" } }, initialize: function(options) { L.setOptions(this, options); this._dataLoaders = {}; // Try to set the zoom control this control is attached to from the // options if (this.options.zoomControl !== null) { this.zoomControl = this.options.zoomControl; } }, onAdd: function(map) { if (this.options.spinjs && (typeof Spinner !== 'function')) { return console.error("Leaflet.loading cannot load because you didn't load spin.js (http://fgnass.github.io/spin.js/), even though you set it in options."); } this._addLayerListeners(map); this._addMapListeners(map); // Try to set the zoom control this control is attached to from the map // the control is being added to if (!this.options.separate && !this.zoomControl) { if (map.zoomControl) { this.zoomControl = map.zoomControl; } else if (map.zoomsliderControl) { this.zoomControl = map.zoomsliderControl; } } // Create the loading indicator var classes = 'leaflet-control-loading'; var container; if (this.zoomControl && !this.options.separate) { // If there is a zoom control, hook into the bottom of it container = this.zoomControl._container; // These classes are no longer used as of Leaflet 0.6 classes += ' leaflet-bar-part-bottom leaflet-bar-part last'; // Loading control will be added to the zoom control. So the visible last element is not the // last dom element anymore. So add the part-bottom class. L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom'); } else { // Otherwise, create a container for the indicator container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-control-layer-container leaflet-bar'); } this._indicatorContainer = container; this._indicator = L.DomUtil.create('a', classes, container); if (this.options.spinjs) { this._spinner = new Spinner(this.options.spin).spin(); this._indicator.appendChild(this._spinner.el); } return container; }, onRemove: function(map) { this._removeLayerListeners(map); this._removeMapListeners(map); }, removeFrom: function (map) { if (this.zoomControl && !this.options.separate) { // Override Control.removeFrom() to avoid clobbering the entire // _container, which is the same as zoomControl's this._container.removeChild(this._indicator); this._map = null; this.onRemove(map); return this; } else { // If this control is separate from the zoomControl, call the // parent method so we don't leave behind an empty container return L.Control.prototype.removeFrom.call(this, map); } }, addLoader: function(id) { this._dataLoaders[id] = true; if (this.options.delayIndicator && !this.delayIndicatorTimeout) { // If we are delaying showing the indicator and we're not // already waiting for that delay, set up a timeout. var that = this; this.delayIndicatorTimeout = setTimeout(function () { that.updateIndicator(); that.delayIndicatorTimeout = null; }, this.options.delayIndicator); } else { // Otherwise show the indicator immediately this.updateIndicator(); } }, removeLoader: function(id) { delete this._dataLoaders[id]; this.updateIndicator(); // If removing this loader means we're in no danger of loading, // clear the timeout. This prevents old delays from instantly // triggering the indicator. if (this.options.delayIndicator && this.delayIndicatorTimeout && !this.isLoading()) { clearTimeout(this.delayIndicatorTimeout); this.delayIndicatorTimeout = null; } }, updateIndicator: function() { if (this.isLoading()) { this._showIndicator(); } else { this._hideIndicator(); } }, isLoading: function() { return this._countLoaders() > 0; }, _countLoaders: function() { var size = 0, key; for (key in this._dataLoaders) { if (this._dataLoaders.hasOwnProperty(key)) size++; } return size; }, _showIndicator: function() { // Show loading indicator L.DomUtil.addClass(this._indicator, 'is-loading'); L.DomUtil.addClass(this._indicatorContainer, 'is-loading'); // If zoomControl exists, make the zoom-out button not last if (!this.options.separate) { if (this.zoomControl instanceof L.Control.Zoom) { L.DomUtil.removeClass(this._getLastControlButton(), 'leaflet-bar-part-bottom'); } else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) { L.DomUtil.removeClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom'); } } }, _hideIndicator: function() { // Hide loading indicator L.DomUtil.removeClass(this._indicator, 'is-loading'); L.DomUtil.removeClass(this._indicatorContainer, 'is-loading'); // If zoomControl exists, make the zoom-out button last if (!this.options.separate) { if (this.zoomControl instanceof L.Control.Zoom) { L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom'); } else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) { L.DomUtil.addClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom'); } } }, _getLastControlButton: function() { var container = this.zoomControl._container, index = container.children.length - 1; // Find the last visible control button that is not our loading // indicator while (index > 0) { var button = container.children[index]; if (!(this._indicator === button || button.offsetWidth === 0 || button.offsetHeight === 0)) { break; } index--; } return container.children[index]; }, _handleLoading: function(e) { this.addLoader(this.getEventId(e)); }, _handleBaseLayerChange: function (e) { var that = this; // Check for a target 'layer' that contains multiple layers, such as // L.LayerGroup. This will happen if you have an L.LayerGroup in an // L.Control.Layers. if (e.layer && e.layer.eachLayer && typeof e.layer.eachLayer === 'function') { e.layer.eachLayer(function (layer) { that._handleBaseLayerChange({ layer: layer }); }); } else { // If we're changing to a canvas layer, don't handle loading // as canvas layers will not fire load events. if (!(L.TileLayer.Canvas && e.layer instanceof L.TileLayer.Canvas)) { that._handleLoading(e); } } }, _handleLoad: function(e) { this.removeLoader(this.getEventId(e)); }, getEventId: function(e) { if (e.id) { return e.id; } else if (e.layer) { return e.layer._leaflet_id; } return e.target._leaflet_id; }, _layerAdd: function(e) { if (!e.layer || !e.layer.on) return try { e.layer.on({ loading: this._handleLoading, load: this._handleLoad }, this); } catch (exception) { console.warn('L.Control.Loading: Tried and failed to add ' + ' event handlers to layer', e.layer); console.warn('L.Control.Loading: Full details', exception); } }, _layerRemove: function(e) { if (!e.layer || !e.layer.off) return; try { e.layer.off({ loading: this._handleLoading, load: this._handleLoad }, this); } catch (exception) { console.warn('L.Control.Loading: Tried and failed to remove ' + 'event handlers from layer', e.layer); console.warn('L.Control.Loading: Full details', exception); } }, _addLayerListeners: function(map) { // Add listeners for begin and end of load to any layers already on the // map map.eachLayer(function(layer) { if (!layer.on) return; layer.on({ loading: this._handleLoading, load: this._handleLoad }, this); }, this); // When a layer is added to the map, add listeners for begin and end // of load map.on('layeradd', this._layerAdd, this); map.on('layerremove', this._layerRemove, this); }, _removeLayerListeners: function(map) { // Remove listeners for begin and end of load from all layers map.eachLayer(function(layer) { if (!layer.off) return; layer.off({ loading: this._handleLoading, load: this._handleLoad }, this); }, this); // Remove layeradd/layerremove listener from map map.off('layeradd', this._layerAdd, this); map.off('layerremove', this._layerRemove, this); }, _addMapListeners: function(map) { // Add listeners to the map for (custom) dataloading and dataload // events, eg, for AJAX calls that affect the map but will not be // reflected in the above layer events. map.on({ baselayerchange: this._handleBaseLayerChange, dataloading: this._handleLoading, dataload: this._handleLoad, layerremove: this._handleLoad }, this); }, _removeMapListeners: function(map) { map.off({ baselayerchange: this._handleBaseLayerChange, dataloading: this._handleLoading, dataload: this._handleLoad, layerremove: this._handleLoad }, this); } }); L.Map.addInitHook(function () { if (this.options.loadingControl) { this.loadingControl = new L.Control.Loading(); this.addControl(this.loadingControl); } }); L.Control.loading = function(options) { return new L.Control.Loading(options); }; } if (typeof define === 'function' && define.amd) { // Try to add leaflet.loading to Leaflet using AMD define(['leaflet'], function (L) { defineLeafletLoading(L); }); } else { // Else use the global L defineLeafletLoading(L); } })();