9a0af4db4e59bd17215d7ddfd11029eecbaed317
[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
118         if (this.options.enableButton || this.options.adjustButton) {
119             this._initializeButtonContainer();
120         }
121
122         if (this.options.enable) {
123             this.enable();
124         }
125     },
126
127     onRemove: function(map) {
128         this.disable();
129         if (this._buttonContainer) {
130             this._buttonContainer.removeFrom(map);
131         }
132     },
133
134     /* Get the current filter bounds */
135     getBounds: function() { 
136         return new L.LatLngBounds(this._sw, this._ne); 
137     },
138
139     setBounds: function(bounds) {
140         this._nw = bounds.getNorthWest();
141         this._ne = bounds.getNorthEast();
142         this._sw = bounds.getSouthWest();
143         this._se = bounds.getSouthEast();
144         if (this.isEnabled()) {
145             this._draw();
146             this.fire("change", {bounds: bounds});
147         }
148     },
149
150     isEnabled: function() {
151         return this._enabled;
152     },
153
154     /* Draw a rectangle */
155     _drawRectangle: function(bounds, options) {
156         options = options || {};
157         var defaultOptions = {
158             stroke: false,
159             fill: true,
160             fillColor: "black",
161             fillOpacity: 0.3,
162             clickable: false
163         };
164         options = L.Util.extend(defaultOptions, options);
165         var rect = new L.Rectangle(bounds, options);
166         rect.addTo(this._layer);
167         return rect;
168     },
169
170     /* Draw a draggable marker */
171     _drawImageMarker: function(point, options) {
172         var marker = new L.Marker(point, {
173             icon: new L.DivIcon({
174                 iconAnchor: options.anchor,
175                 iconSize: options.size,
176                 className: options.className
177             }),
178             draggable: true
179         });
180         marker.addTo(this._layer);
181         return marker;
182     },
183
184     /* Draw a move marker. Sets up drag listener that updates the
185        filter corners and redraws the filter when the move marker is
186        moved */
187     _drawMoveMarker: function(point) {
188         var that = this;
189         this._moveMarker = this._drawImageMarker(point, {
190             "className": "location-filter move-marker",
191             "anchor": [-10, -10],
192             "size": [13,13]
193         });
194         this._moveMarker.on('drag', function(e) {
195             var markerPos = that._moveMarker.getLatLng(),
196                 latDelta = markerPos.lat-that._nw.lat,
197                 lngDelta = markerPos.lng-that._nw.lng;
198             that._nw = new L.LatLng(that._nw.lat+latDelta, that._nw.lng+lngDelta, true);
199             that._ne = new L.LatLng(that._ne.lat+latDelta, that._ne.lng+lngDelta, true);
200             that._sw = new L.LatLng(that._sw.lat+latDelta, that._sw.lng+lngDelta, true);
201             that._se = new L.LatLng(that._se.lat+latDelta, that._se.lng+lngDelta, true);
202             that._draw();
203         });
204         this._setupDragendListener(this._moveMarker);
205         return this._moveMarker;
206     },
207
208     /* Draw a resize marker */
209     _drawResizeMarker: function(point, latFollower, lngFollower, otherPos) {
210         return this._drawImageMarker(point, {
211             "className": "location-filter resize-marker",
212             "anchor": [7, 6],
213             "size": [13, 12] 
214         });
215     },
216
217     /* Track moving of the given resize marker and update the markers
218        given in options.moveAlong to match the position of the moved
219        marker. Update filter corners and redraw the filter */
220     _setupResizeMarkerTracking: function(marker, options) {
221         var that = this;
222         marker.on('drag', function(e) {
223             var curPosition = marker.getLatLng(),
224                 latMarker = options.moveAlong.lat,
225                 lngMarker = options.moveAlong.lng;
226             // Move follower markers when this marker is moved
227             latMarker.setLatLng(new L.LatLng(curPosition.lat, latMarker.getLatLng().lng, true));
228             lngMarker.setLatLng(new L.LatLng(lngMarker.getLatLng().lat, curPosition.lng, true));
229             // Sort marker positions in nw, ne, sw, se order
230             var corners = [that._nwMarker.getLatLng(), 
231                            that._neMarker.getLatLng(), 
232                            that._swMarker.getLatLng(), 
233                            that._seMarker.getLatLng()];
234             corners.sort(function(a, b) {
235                 if (a.lat != b.lat)
236                     return b.lat-a.lat;
237                 else
238                     return a.lng-b.lng;
239             });
240             // Update corner points and redraw everything except the resize markers
241             that._nw = corners[0];
242             that._ne = corners[1];
243             that._sw = corners[2];
244             that._se = corners[3];
245             that._draw({repositionResizeMarkers: false});
246         });
247         this._setupDragendListener(marker);
248     },
249
250     /* Emit a change event whenever dragend is triggered on the
251        given marker */
252     _setupDragendListener: function(marker) {
253         var that = this;
254         marker.on('dragend', function(e) {
255             that.fire("change", {bounds: that.getBounds()});
256         });
257     },
258
259     /* Create bounds for the mask rectangles and the location
260        filter rectangle */
261     _calculateBounds: function() {
262         var mapBounds = this._map.getBounds(),
263             outerBounds = new L.LatLngBounds(
264                 new L.LatLng(mapBounds.getSouthWest().lat-0.1,
265                              mapBounds.getSouthWest().lng-0.1, true),
266                 new L.LatLng(mapBounds.getNorthEast().lat+0.1,
267                              mapBounds.getNorthEast().lng+0.1, true)
268             );
269
270         // The south west and north east points of the mask */
271         this._osw = outerBounds.getSouthWest();
272         this._one = outerBounds.getNorthEast();
273
274         // Bounds for the mask rectangles
275         this._northBounds = new L.LatLngBounds(new L.LatLng(this._ne.lat, this._osw.lng, true), this._one);
276         this._westBounds = new L.LatLngBounds(new L.LatLng(this._sw.lat, this._osw.lng, true), this._nw);
277         this._eastBounds = new L.LatLngBounds(this._se, new L.LatLng(this._ne.lat, this._one.lng, true));
278         this._southBounds = new L.LatLngBounds(this._osw, new L.LatLng(this._sw.lat, this._one.lng, true));
279     },
280
281     /* Initializes rectangles and markers */
282     _initialDraw: function() {
283         if (this._initialDrawCalled) {
284             return;
285         }
286
287         this._layer = new L.LayerGroup();
288
289         // Calculate filter bounds
290         this._calculateBounds();
291
292         // Create rectangles
293         this._northRect = this._drawRectangle(this._northBounds);
294         this._westRect = this._drawRectangle(this._westBounds);
295         this._eastRect = this._drawRectangle(this._eastBounds);
296         this._southRect = this._drawRectangle(this._southBounds);
297         this._innerRect = this._drawRectangle(this.getBounds(), {
298             fillOpacity: 0,
299             stroke: true,
300             color: "white",
301             weight: 1,
302             opacity: 0.9
303         });
304
305         // Create resize markers
306         this._nwMarker = this._drawResizeMarker(this._nw);
307         this._neMarker = this._drawResizeMarker(this._ne);
308         this._swMarker = this._drawResizeMarker(this._sw);
309         this._seMarker = this._drawResizeMarker(this._se);
310
311         // Setup tracking of resize markers. Each marker has pair of
312         // follower markers that must be moved whenever the marker is
313         // moved. For example, whenever the north west resize marker
314         // moves, the south west marker must move along on the x-axis
315         // and the north east marker must move on the y axis
316         this._setupResizeMarkerTracking(this._nwMarker, {moveAlong: {lat: this._neMarker, lng: this._swMarker}});
317         this._setupResizeMarkerTracking(this._neMarker, {moveAlong: {lat: this._nwMarker, lng: this._seMarker}});
318         this._setupResizeMarkerTracking(this._swMarker, {moveAlong: {lat: this._seMarker, lng: this._nwMarker}});
319         this._setupResizeMarkerTracking(this._seMarker, {moveAlong: {lat: this._swMarker, lng: this._neMarker}});
320
321         // Create move marker
322         this._moveMarker = this._drawMoveMarker(this._nw);
323
324         this._initialDrawCalled = true;
325     },
326
327     /* Reposition all rectangles and markers to the current filter bounds. */    
328     _draw: function(options) {
329         options = L.Util.extend({repositionResizeMarkers: true}, options);
330
331         // Calculate filter bounds
332         this._calculateBounds();
333
334         // Reposition rectangles
335         this._northRect.setBounds(this._northBounds);
336         this._westRect.setBounds(this._westBounds);
337         this._eastRect.setBounds(this._eastBounds);
338         this._southRect.setBounds(this._southBounds);
339         this._innerRect.setBounds(this.getBounds());
340
341         // Reposition resize markers
342         if (options.repositionResizeMarkers) {
343             this._nwMarker.setLatLng(this._nw);
344             this._neMarker.setLatLng(this._ne);
345             this._swMarker.setLatLng(this._sw);
346             this._seMarker.setLatLng(this._se);
347         }
348
349         // Reposition the move marker
350         this._moveMarker.setLatLng(this._nw);
351     }, 
352
353     /* Adjust the location filter to the current map bounds */
354     _adjustToMap: function() {
355         this.setBounds(this._map.getBounds());
356         this._map.zoomOut();
357     },
358
359     /* Enable the location filter */
360     enable: function() {
361         if (this._enabled) {
362             return;
363         }
364
365         // Initialize corners
366         var bounds;
367         if (this._sw && this._ne) {
368             bounds = new L.LatLngBounds(this._sw, this._ne);
369         } else if (this.options.bounds) {
370             bounds = this.options.bounds;
371         } else {
372             bounds = this._map.getBounds();
373         }
374         this._map.invalidateSize();
375         this._nw = bounds.getNorthWest();
376         this._ne = bounds.getNorthEast();
377         this._sw = bounds.getSouthWest();
378         this._se = bounds.getSouthEast();
379             
380
381         // Update buttons
382         if (this._buttonContainer) {
383             this._buttonContainer.addClass("enabled");
384         }
385
386         if (this._enableButton) {
387             this._enableButton.setText(this.options.enableButton.disableText);
388         }
389
390         if (this.options.adjustButton) {
391             this._createAdjustButton();
392         }
393         
394         // Draw filter
395         this._initialDraw();
396         this._draw();
397
398         // Set up map move event listener
399         var that = this;
400         this._moveHandler = function() {
401             that._draw();
402         };
403         this._map.on("move", this._moveHandler);
404
405         // Add the filter layer to the map
406         this._layer.addTo(this._map);
407         
408         // Zoom out the map if necessary
409         var mapBounds = this._map.getBounds();
410         bounds = new L.LatLngBounds(this._sw, this._ne).modify(this._map, 10);
411         if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast())) {
412             this._map.fitBounds(bounds);
413         }
414
415         this._enabled = true;
416         
417         // Fire the enabled event
418         this.fire("enabled");
419     },
420
421     /* Disable the location filter */
422     disable: function() {
423         if (!this._enabled) {
424             return;
425         }
426
427         // Update buttons
428         if (this._buttonContainer) {
429             this._buttonContainer.removeClass("enabled");
430         }
431
432         if (this._enableButton) {
433             this._enableButton.setText(this.options.enableButton.enableText);
434         }
435
436         if (this._adjustButton) {
437             this._adjustButton.remove();
438         }
439
440         // Remove event listener
441         this._map.off("move", this._moveHandler);
442
443         // Remove rectangle layer from map
444         this._map.removeLayer(this._layer);
445
446         this._enabled = false;
447
448         // Fire the disabled event
449         this.fire("disabled");
450     },
451
452     /* Create a button that allows the user to adjust the location
453        filter to the current zoom */
454     _createAdjustButton: function() {
455         var that = this;
456         this._adjustButton = new L.Control.Button({
457             className: "adjust-button",
458             text: this.options.adjustButton.text,
459             
460             onClick: function(event) {
461                 that._adjustToMap();
462                 that.fire("adjustToZoomClick");
463             }
464         }).addTo(this._buttonContainer);
465     },
466
467     /* Create the location filter button container and the button that
468        toggles the location filter */
469     _initializeButtonContainer: function() {
470         var that = this;
471         this._buttonContainer = new L.Control.ButtonContainer({className: "location-filter button-container"});
472
473         if (this.options.enableButton) {
474             this._enableButton = new L.Control.Button({
475                 className: "enable-button",
476                 text: this.options.enableButton.enableText,
477
478                 onClick: function(event) {
479                     if (!that._enabled) {
480                         // Enable the location filter
481                         that.enable();
482                         that.fire("enableClick");
483                     } else {
484                         // Disable the location filter
485                         that.disable();
486                         that.fire("disableClick");
487                     }
488                 }
489             }).addTo(this._buttonContainer);
490         }
491
492         this._buttonContainer.addTo(this._map);
493     }
494 });