foist
[kismet-logviewer.git] / logviewer / static / js / Control.Loading.js
1 /*
2  * L.Control.Loading is a control that shows a loading indicator when tiles are
3  * loading or when map-related AJAX requests are taking place.
4  */
5
6 (function () {
7
8     var console = window.console || {
9         error: function () {},
10         warn: function () {}
11     };
12
13     function defineLeafletLoading(L) {
14         L.Control.Loading = L.Control.extend({
15             options: {
16                 delayIndicator: null,
17                 position: 'topleft',
18                 separate: false,
19                 zoomControl: null,
20                 spinjs: false,
21                 spin: {
22                     lines: 7,
23                     length: 3,
24                     width: 3,
25                     radius: 5,
26                     rotate: 13,
27                     top: "83%"
28                 }
29             },
30
31             initialize: function(options) {
32                 L.setOptions(this, options);
33                 this._dataLoaders = {};
34
35                 // Try to set the zoom control this control is attached to from
36                 // the options
37                 if (this.options.zoomControl !== null) {
38                     this.zoomControl = this.options.zoomControl;
39                 }
40             },
41
42             onAdd: function(map) {
43                 if (this.options.spinjs && (typeof Spinner !== 'function')) {
44                     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.");
45                 }
46                 this._addLayerListeners(map);
47                 this._addMapListeners(map);
48
49                 // Try to set the zoom control this control is attached to from the map
50                 // the control is being added to
51                 if (!this.options.separate && !this.zoomControl) {
52                     if (map.zoomControl) {
53                         this.zoomControl = map.zoomControl;
54                     } else if (map.zoomsliderControl) {
55                         this.zoomControl = map.zoomsliderControl;
56                     }
57                 }
58
59                 // Create the loading indicator
60                 var classes = 'leaflet-control-loading';
61                 var container;
62                 if (this.zoomControl && !this.options.separate) {
63                     // If there is a zoom control, hook into the bottom of it
64                     container = this.zoomControl._container;
65                     // These classes are no longer used as of Leaflet 0.6
66                     classes += ' leaflet-bar-part-bottom leaflet-bar-part last';
67
68                     // Loading control will be added to the zoom control. So the visible last element is not the
69                     // last dom element anymore. So add the part-bottom class.
70                     L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
71                 }
72                 else {
73                     // Otherwise, create a container for the indicator
74                     container = L.DomUtil.create('div', 'leaflet-control-zoom leaflet-control-layer-container leaflet-bar');
75                 }
76                 this._indicatorContainer = container;
77                 this._indicator = L.DomUtil.create('a', classes, container);
78                 if (this.options.spinjs) {
79                     this._spinner = new Spinner(this.options.spin).spin();
80                     this._indicator.appendChild(this._spinner.el);
81                 }
82                 return container;
83             },
84
85             onRemove: function(map) {
86                 this._removeLayerListeners(map);
87                 this._removeMapListeners(map);
88             },
89
90             removeFrom: function (map) {
91                 if (this.zoomControl && !this.options.separate) {
92                     // Override Control.removeFrom() to avoid clobbering the entire
93                     // _container, which is the same as zoomControl's
94                     this._container.removeChild(this._indicator);
95                     this._map = null;
96                     this.onRemove(map);
97                     return this;
98                 }
99                 else {
100                     // If this control is separate from the zoomControl, call the
101                     // parent method so we don't leave behind an empty container
102                     return L.Control.prototype.removeFrom.call(this, map);
103                 }
104             },
105
106             addLoader: function(id) {
107                 this._dataLoaders[id] = true;
108                 if (this.options.delayIndicator && !this.delayIndicatorTimeout) {
109                     // If we are delaying showing the indicator and we're not
110                     // already waiting for that delay, set up a timeout.
111                     var that = this;
112                     this.delayIndicatorTimeout = setTimeout(function () {
113                         that.updateIndicator();
114                         that.delayIndicatorTimeout = null;
115                     }, this.options.delayIndicator);
116                 }
117                 else {
118                     // Otherwise show the indicator immediately
119                     this.updateIndicator();
120                 }
121             },
122
123             removeLoader: function(id) {
124                 delete this._dataLoaders[id];
125                 this.updateIndicator();
126
127                 // If removing this loader means we're in no danger of loading,
128                 // clear the timeout. This prevents old delays from instantly
129                 // triggering the indicator.
130                 if (this.options.delayIndicator && this.delayIndicatorTimeout && !this.isLoading()) {
131                     clearTimeout(this.delayIndicatorTimeout);
132                     this.delayIndicatorTimeout = null;
133                 }
134             },
135
136             updateIndicator: function() {
137                 if (this.isLoading()) {
138                     this._showIndicator();
139                 }
140                 else {
141                     this._hideIndicator();
142                 }
143             },
144
145             isLoading: function() {
146                 return this._countLoaders() > 0;
147             },
148
149             _countLoaders: function() {
150                 var size = 0, key;
151                 for (key in this._dataLoaders) {
152                     if (this._dataLoaders.hasOwnProperty(key)) size++;
153                 }
154                 return size;
155             },
156
157             _showIndicator: function() {
158                 // Show loading indicator
159                 L.DomUtil.addClass(this._indicator, 'is-loading');
160                 L.DomUtil.addClass(this._indicatorContainer, 'is-loading');
161
162                 // If zoomControl exists, make the zoom-out button not last
163                 if (!this.options.separate) {
164                     if (this.zoomControl instanceof L.Control.Zoom) {
165                         L.DomUtil.removeClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
166                     }
167                     else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) {
168                         L.DomUtil.removeClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom');
169                     }
170                 }
171             },
172
173             _hideIndicator: function() {
174                 // Hide loading indicator
175                 L.DomUtil.removeClass(this._indicator, 'is-loading');
176                 L.DomUtil.removeClass(this._indicatorContainer, 'is-loading');
177
178                 // If zoomControl exists, make the zoom-out button last
179                 if (!this.options.separate) {
180                     if (this.zoomControl instanceof L.Control.Zoom) {
181                         L.DomUtil.addClass(this._getLastControlButton(), 'leaflet-bar-part-bottom');
182                     }
183                     else if (typeof L.Control.Zoomslider === 'function' && this.zoomControl instanceof L.Control.Zoomslider) {
184                         L.DomUtil.addClass(this.zoomControl._ui.zoomOut, 'leaflet-bar-part-bottom');
185                     }
186                 }
187             },
188
189             _getLastControlButton: function() {
190                 var container = this.zoomControl._container,
191                     index = container.children.length - 1;
192
193                 // Find the last visible control button that is not our loading
194                 // indicator
195                 while (index > 0) {
196                     var button = container.children[index];
197                     if (!(this._indicator === button || button.offsetWidth === 0 || button.offsetHeight === 0)) {
198                         break;
199                     }
200                     index--;
201                 }
202
203                 return container.children[index];
204             },
205
206             _handleLoading: function(e) {
207                 this.addLoader(this.getEventId(e));
208             },
209
210             _handleBaseLayerChange: function (e) {
211                 var that = this;
212
213                 // Check for a target 'layer' that contains multiple layers, such as
214                 // L.LayerGroup. This will happen if you have an L.LayerGroup in an
215                 // L.Control.Layers.
216                 if (e.layer && e.layer.eachLayer && typeof e.layer.eachLayer === 'function') {
217                     e.layer.eachLayer(function (layer) {
218                         that._handleBaseLayerChange({ layer: layer });
219                     });
220                 }
221                 else {
222                     // If we're changing to a canvas layer, don't handle loading
223                     // as canvas layers will not fire load events.
224                     if (!(L.TileLayer.Canvas && e.layer instanceof L.TileLayer.Canvas)) {
225                         that._handleLoading(e);
226                     }
227                 }
228             },
229
230             _handleLoad: function(e) {
231                 this.removeLoader(this.getEventId(e));
232             },
233
234             getEventId: function(e) {
235                 if (e.id) {
236                     return e.id;
237                 }
238                 else if (e.layer) {
239                     return e.layer._leaflet_id;
240                 }
241                 return e.target._leaflet_id;
242             },
243
244             _layerAdd: function(e) {
245                 if (!e.layer || !e.layer.on) return
246                 try {
247                     e.layer.on({
248                         loading: this._handleLoading,
249                         load: this._handleLoad
250                     }, this);
251                 }
252                 catch (exception) {
253                     console.warn('L.Control.Loading: Tried and failed to add ' +
254                                  ' event handlers to layer', e.layer);
255                     console.warn('L.Control.Loading: Full details', exception);
256                 }
257             },
258
259             _layerRemove: function(e) {
260                 if (!e.layer || !e.layer.off) return;
261                 try {
262                     e.layer.off({
263                         loading: this._handleLoading,
264                         load: this._handleLoad
265                     }, this);
266                 }
267                 catch (exception) {
268                     console.warn('L.Control.Loading: Tried and failed to remove ' +
269                                  'event handlers from layer', e.layer);
270                     console.warn('L.Control.Loading: Full details', exception);
271                 }
272             },
273
274             _addLayerListeners: function(map) {
275                 // Add listeners for begin and end of load to any layers already
276                 // on the map
277                 map.eachLayer(function(layer) {
278                     if (!layer.on) return;
279                     layer.on({
280                         loading: this._handleLoading,
281                         load: this._handleLoad
282                     }, this);
283                 }, this);
284
285                 // When a layer is added to the map, add listeners for begin and
286                 // end of load
287                 map.on('layeradd', this._layerAdd, this);
288                 map.on('layerremove', this._layerRemove, this);
289             },
290
291             _removeLayerListeners: function(map) {
292                 // Remove listeners for begin and end of load from all layers
293                 map.eachLayer(function(layer) {
294                     if (!layer.off) return;
295                     layer.off({
296                         loading: this._handleLoading,
297                         load: this._handleLoad
298                     }, this);
299                 }, this);
300
301                 // Remove layeradd/layerremove listener from map
302                 map.off('layeradd', this._layerAdd, this);
303                 map.off('layerremove', this._layerRemove, this);
304             },
305
306             _addMapListeners: function(map) {
307                 // Add listeners to the map for (custom) dataloading and dataload
308                 // events, eg, for AJAX calls that affect the map but will not be
309                 // reflected in the above layer events.
310                 map.on({
311                     baselayerchange: this._handleBaseLayerChange,
312                     dataloading: this._handleLoading,
313                     dataload: this._handleLoad,
314                     layerremove: this._handleLoad
315                 }, this);
316             },
317
318             _removeMapListeners: function(map) {
319                 map.off({
320                     baselayerchange: this._handleBaseLayerChange,
321                     dataloading: this._handleLoading,
322                     dataload: this._handleLoad,
323                     layerremove: this._handleLoad
324                 }, this);
325             }
326         });
327
328         L.Map.addInitHook(function () {
329             if (this.options.loadingControl) {
330                 this.loadingControl = new L.Control.Loading();
331                 this.addControl(this.loadingControl);
332             }
333         });
334
335         L.Control.loading = function(options) {
336             return new L.Control.Loading(options);
337         };
338     }
339
340     if (typeof define === 'function' && define.amd) {
341         // Try to add leaflet.loading to Leaflet using AMD
342         define(['leaflet'], function (L) {
343             defineLeafletLoading(L);
344         });
345     }
346     else {
347         // Else use the global L
348         defineLeafletLoading(L);
349     }
350
351 })();