a17f1a5affbc5e779be21299ef0333c543258448
[rails.git] / vendor / assets / leaflet / leaflet.locationfilter.js
1 /*
2  * Leaflet.locationfilter - leaflet location filter plugin
3  * Copyright (C) 2012, Tripbirds.com
4  * http://tripbirds.com
5  *
6  * Licensed under the MIT License.
7  *
8  * Date: 2012-09-24
9  * Version: 0.1
10  */
11 L.LatLngBounds.prototype.modify = function(map, amount) {
12     var sw = this.getSouthWest(),
13         ne = this.getNorthEast(),
14         swPoint = map.latLngToLayerPoint(sw),
15         nePoint = map.latLngToLayerPoint(ne);
16
17     sw = map.layerPointToLatLng(new L.Point(swPoint.x-amount, swPoint.y+amount));
18     ne = map.layerPointToLatLng(new L.Point(nePoint.x+amount, nePoint.y-amount));
19     
20     return new L.LatLngBounds(sw, ne);
21 };
22
23 L.Control.Button = L.Class.extend({
24     initialize: function(options) {
25         L.Util.setOptions(this, options);
26     },
27
28     addTo: function(container) {
29         container.addButton(this);
30         return this;
31     },
32     
33     onAdd: function (buttonContainer) {
34         this._buttonContainer = buttonContainer;
35         this._button = L.DomUtil.create('a', this.options.className, this._buttonContainer.getContainer());
36         this._button.href = '#';
37         this.setText(this.options.text);
38
39         var that = this;
40         this._onClick = function(event) {
41             that.options.onClick.call(that, event);
42         };
43
44         L.DomEvent
45             .on(this._button, 'click', L.DomEvent.stopPropagation)
46             .on(this._button, 'mousedown', L.DomEvent.stopPropagation)
47             .on(this._button, 'dblclick', L.DomEvent.stopPropagation)
48             .on(this._button, 'click', L.DomEvent.preventDefault)
49             .on(this._button, 'click', this._onClick, this);
50     },
51
52     remove: function() {
53         L.DomEvent.off(this._button, "click", this._onClick);
54         this._buttonContainer.getContainer().removeChild(this._button);
55     },
56
57     setText: function(text) {
58         this._button.title = text;
59         this._button.innerHTML = text;
60     }
61 });
62
63 L.Control.ButtonContainer = L.Control.extend({
64     options: {
65         position: 'topleft'
66     },
67
68     getContainer: function() {
69         if (!this._container) {
70             this._container = L.DomUtil.create('div', this.options.className);
71         }
72         return this._container;
73     },
74
75     onAdd: function (map) {
76         this._map = map;
77         return this.getContainer();
78     },
79
80     addButton: function(button) {
81         button.onAdd(this);
82     },
83
84     addClass: function(className) {
85         L.DomUtil.addClass(this.getContainer(), className);
86     },
87
88     removeClass: function(className) {
89         L.DomUtil.removeClass(this.getContainer(), className);
90     }
91 });
92
93 L.LocationFilter = L.Class.extend({
94     includes: L.Mixin.Events,
95
96     options: {
97         enableButton: {
98             enableText: "Select area",
99             disableText: "Remove selection"
100         },
101         adjustButton: {
102             text: "Select area within current zoom"
103         }
104     },
105
106     initialize: function(options) {
107         L.Util.setOptions(this, options);
108     },
109
110     addTo: function(map) {
111         map.addLayer(this);
112         return this;
113     },
114
115     onAdd: function(map) {
116         this._map = map;
117         this._layer = new L.LayerGroup();
118
119         if (this.options.enableButton || this.options.adjustButton) {
120             this._initializeButtonContainer();
121         }
122
123         if (this.options.enable) {
124             this.enable();
125         }
126     },
127
128     onRemove: function(map) {
129         this.disable();
130         if (this._buttonContainer) {
131             this._buttonContainer.removeFrom(map);
132         }
133     },
134
135     /* Get the current filter bounds */
136     getBounds: function() { 
137         return new L.LatLngBounds(this._sw, this._ne); 
138     },
139
140     setBounds: function(bounds) {
141         this._nw = bounds.getNorthWest();
142         this._ne = bounds.getNorthEast();
143         this._sw = bounds.getSouthWest();
144         this._se = bounds.getSouthEast();
145         if (this.isEnabled()) {
146             this._draw();
147             this.fire("change", {bounds: bounds});
148         }
149     },
150
151     isEnabled: function() {
152         return this._enabled;
153     },
154
155     /* Draw a rectangle */
156     _drawRectangle: function(bounds, options) {
157         options = options || {};
158         var defaultOptions = {
159             stroke: false,
160             fill: true,
161             fillColor: "black",
162             fillOpacity: 0.3,
163             clickable: false
164         };
165         options = L.Util.extend(defaultOptions, options);
166         var rect = new L.Rectangle(bounds, options);
167         rect.addTo(this._layer);
168         return rect;
169     },
170
171     /* Draw a draggable marker */
172     _drawImageMarker: function(point, options) {
173         var marker = new L.Marker(point, {
174             icon: new L.DivIcon({
175                 iconAnchor: options.anchor,
176                 iconSize: options.size,
177                 className: options.className
178             }),
179             draggable: true
180         });
181         marker.addTo(this._layer);
182         return marker;
183     },
184
185     /* Draw a move marker. Sets up drag listener that updates the
186        filter corners and redraws the filter when the move marker is
187        moved */
188     _drawMoveMarker: function(point) {
189         var that = this;
190         this._moveMarker = this._drawImageMarker(point, {
191             "className": "location-filter move-marker",
192             "anchor": [-10, -10],
193             "size": [13,13]
194         });
195         this._moveMarker.on('drag', function(e) {
196             var markerPos = that._moveMarker.getLatLng(),
197                 latDelta = markerPos.lat-that._nw.lat,
198                 lngDelta = markerPos.lng-that._nw.lng;
199             that._nw = new L.LatLng(that._nw.lat+latDelta, that._nw.lng+lngDelta, true);
200             that._ne = new L.LatLng(that._ne.lat+latDelta, that._ne.lng+lngDelta, true);
201             that._sw = new L.LatLng(that._sw.lat+latDelta, that._sw.lng+lngDelta, true);
202             that._se = new L.LatLng(that._se.lat+latDelta, that._se.lng+lngDelta, true);
203             that._draw();
204         });
205         this._setupDragendListener(this._moveMarker);
206         return this._moveMarker;
207     },
208
209     /* Draw a resize marker */
210     _drawResizeMarker: function(point, latFollower, lngFollower, otherPos) {
211         return this._drawImageMarker(point, {
212             "className": "location-filter resize-marker",
213             "anchor": [7, 6],
214             "size": [13, 12] 
215         });
216     },
217
218     /* Track moving of the given resize marker and update the markers
219        given in options.moveAlong to match the position of the moved
220        marker. Update filter corners and redraw the filter */
221     _setupResizeMarkerTracking: function(marker, options) {
222         var that = this;
223         marker.on('drag', function(e) {
224             var curPosition = marker.getLatLng(),
225                 latMarker = options.moveAlong.lat,
226                 lngMarker = options.moveAlong.lng;
227             // Move follower markers when this marker is moved
228             latMarker.setLatLng(new L.LatLng(curPosition.lat, latMarker.getLatLng().lng, true));
229             lngMarker.setLatLng(new L.LatLng(lngMarker.getLatLng().lat, curPosition.lng, true));
230             // Sort marker positions in nw, ne, sw, se order
231             var corners = [that._nwMarker.getLatLng(), 
232                            that._neMarker.getLatLng(), 
233                            that._swMarker.getLatLng(), 
234                            that._seMarker.getLatLng()];
235             corners.sort(function(a, b) {
236                 if (a.lat != b.lat)
237                     return b.lat-a.lat;
238                 else
239                     return a.lng-b.lng;
240             });
241             // Update corner points and redraw everything except the resize markers
242             that._nw = corners[0];
243             that._ne = corners[1];
244             that._sw = corners[2];
245             that._se = corners[3];
246             that._draw({repositionResizeMarkers: false});
247         });
248         this._setupDragendListener(marker);
249     },
250
251     /* Emit a change event whenever dragend is triggered on the
252        given marker */
253     _setupDragendListener: function(marker) {
254         var that = this;
255         marker.on('dragend', function(e) {
256             that.fire("change", {bounds: that.getBounds()});
257         });
258     },
259
260     /* Create bounds for the mask rectangles and the location
261        filter rectangle */
262     _calculateBounds: function() {
263         var mapBounds = this._map.getBounds(),
264             outerBounds = new L.LatLngBounds(
265                 new L.LatLng(mapBounds.getSouthWest().lat-0.1,
266                              mapBounds.getSouthWest().lng-0.1, true),
267                 new L.LatLng(mapBounds.getNorthEast().lat+0.1,
268                              mapBounds.getNorthEast().lng+0.1, true)
269             );
270
271         // The south west and north east points of the mask */
272         this._osw = outerBounds.getSouthWest();
273         this._one = outerBounds.getNorthEast();
274
275         // Bounds for the mask rectangles
276         this._northBounds = new L.LatLngBounds(new L.LatLng(this._ne.lat, this._osw.lng, true), this._one);
277         this._westBounds = new L.LatLngBounds(new L.LatLng(this._sw.lat, this._osw.lng, true), this._nw);
278         this._eastBounds = new L.LatLngBounds(this._se, new L.LatLng(this._ne.lat, this._one.lng, true));
279         this._southBounds = new L.LatLngBounds(this._osw, new L.LatLng(this._sw.lat, this._one.lng, true));
280     },
281
282     /* Initializes rectangles and markers */
283     _initialDraw: function() {
284         if (this._initialDrawCalled) {
285             return;
286         }
287
288         // Calculate filter bounds
289         this._calculateBounds();
290
291         // Create rectangles
292         this._northRect = this._drawRectangle(this._northBounds);
293         this._westRect = this._drawRectangle(this._westBounds);
294         this._eastRect = this._drawRectangle(this._eastBounds);
295         this._southRect = this._drawRectangle(this._southBounds);
296         this._innerRect = this._drawRectangle(this.getBounds(), {
297             fillOpacity: 0,
298             stroke: true,
299             color: "white",
300             weight: 1,
301             opacity: 0.9
302         });
303
304         // Create resize markers
305         this._nwMarker = this._drawResizeMarker(this._nw);
306         this._neMarker = this._drawResizeMarker(this._ne);
307         this._swMarker = this._drawResizeMarker(this._sw);
308         this._seMarker = this._drawResizeMarker(this._se);
309
310         // Setup tracking of resize markers. Each marker has pair of
311         // follower markers that must be moved whenever the marker is
312         // moved. For example, whenever the north west resize marker
313         // moves, the south west marker must move along on the x-axis
314         // and the north east marker must move on the y axis
315         this._setupResizeMarkerTracking(this._nwMarker, {moveAlong: {lat: this._neMarker, lng: this._swMarker}});
316         this._setupResizeMarkerTracking(this._neMarker, {moveAlong: {lat: this._nwMarker, lng: this._seMarker}});
317         this._setupResizeMarkerTracking(this._swMarker, {moveAlong: {lat: this._seMarker, lng: this._nwMarker}});
318         this._setupResizeMarkerTracking(this._seMarker, {moveAlong: {lat: this._swMarker, lng: this._neMarker}});
319
320         // Create move marker
321         this._moveMarker = this._drawMoveMarker(this._nw);
322
323         this._initialDrawCalled = true;
324     },
325
326     /* Reposition all rectangles and markers to the current filter bounds. */    
327     _draw: function(options) {
328         options = L.Util.extend({repositionResizeMarkers: true}, options);
329
330         // Calculate filter bounds
331         this._calculateBounds();
332
333         // Reposition rectangles
334         this._northRect.setBounds(this._northBounds);
335         this._westRect.setBounds(this._westBounds);
336         this._eastRect.setBounds(this._eastBounds);
337         this._southRect.setBounds(this._southBounds);
338         this._innerRect.setBounds(this.getBounds());
339
340         // Reposition resize markers
341         if (options.repositionResizeMarkers) {
342             this._nwMarker.setLatLng(this._nw);
343             this._neMarker.setLatLng(this._ne);
344             this._swMarker.setLatLng(this._sw);
345             this._seMarker.setLatLng(this._se);
346         }
347
348         // Reposition the move marker
349         this._moveMarker.setLatLng(this._nw);
350     }, 
351
352     /* Adjust the location filter to the current map bounds */
353     _adjustToMap: function() {
354         this.setBounds(this._map.getBounds());
355         this._map.zoomOut();
356     },
357
358     /* Enable the location filter */
359     enable: function() {
360         if (this._enabled) {
361             return;
362         }
363
364         // Initialize corners
365         var bounds;
366         if (this._sw && this._ne) {
367             bounds = new L.LatLngBounds(this._sw, this._ne);
368         } else if (this.options.bounds) {
369             bounds = this.options.bounds;
370         } else {
371             bounds = this._map.getBounds();
372         }
373         this._map.invalidateSize();
374         this._nw = bounds.getNorthWest();
375         this._ne = bounds.getNorthEast();
376         this._sw = bounds.getSouthWest();
377         this._se = bounds.getSouthEast();
378             
379
380         // Update buttons
381         if (this._buttonContainer) {
382             this._buttonContainer.addClass("enabled");
383         }
384
385         if (this._enableButton) {
386             this._enableButton.setText(this.options.enableButton.disableText);
387         }
388
389         if (this.options.adjustButton) {
390             this._createAdjustButton();
391         }
392         
393         // Draw filter
394         this._initialDraw();
395         this._draw();
396
397         // Set up map move event listener
398         var that = this;
399         this._moveHandler = function() {
400             that._draw();
401         };
402         this._map.on("move", this._moveHandler);
403
404         // Add the filter layer to the map
405         this._layer.addTo(this._map);
406         
407         // Zoom out the map if necessary
408         var mapBounds = this._map.getBounds();
409         bounds = new L.LatLngBounds(this._sw, this._ne).modify(this._map, 10);
410         if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast())) {
411             this._map.fitBounds(bounds);
412         }
413
414         this._enabled = true;
415         
416         // Fire the enabled event
417         this.fire("enabled");
418     },
419
420     /* Disable the location filter */
421     disable: function() {
422         if (!this._enabled) {
423             return;
424         }
425
426         // Update buttons
427         if (this._buttonContainer) {
428             this._buttonContainer.removeClass("enabled");
429         }
430
431         if (this._enableButton) {
432             this._enableButton.setText(this.options.enableButton.enableText);
433         }
434
435         if (this._adjustButton) {
436             this._adjustButton.remove();
437         }
438
439         // Remove event listener
440         this._map.off("move", this._moveHandler);
441
442         // Remove rectangle layer from map
443         this._map.removeLayer(this._layer);
444
445         this._enabled = false;
446
447         // Fire the disabled event
448         this.fire("disabled");
449     },
450
451     /* Create a button that allows the user to adjust the location
452        filter to the current zoom */
453     _createAdjustButton: function() {
454         var that = this;
455         this._adjustButton = new L.Control.Button({
456             className: "adjust-button",
457             text: this.options.adjustButton.text,
458             
459             onClick: function(event) {
460                 that._adjustToMap();
461                 that.fire("adjustToZoomClick");
462             }
463         }).addTo(this._buttonContainer);
464     },
465
466     /* Create the location filter button container and the button that
467        toggles the location filter */
468     _initializeButtonContainer: function() {
469         var that = this;
470         this._buttonContainer = new L.Control.ButtonContainer({className: "location-filter button-container"});
471
472         if (this.options.enableButton) {
473             this._enableButton = new L.Control.Button({
474                 className: "enable-button",
475                 text: this.options.enableButton.enableText,
476
477                 onClick: function(event) {
478                     if (!that._enabled) {
479                         // Enable the location filter
480                         that.enable();
481                         that.fire("enableClick");
482                     } else {
483                         // Disable the location filter
484                         that.disable();
485                         that.fire("disableClick");
486                     }
487                 }
488             }).addTo(this._buttonContainer);
489         }
490
491         this._buttonContainer.addTo(this._map);
492     }
493 });