]> git.openstreetmap.org Git - rails.git/blob - vendor/assets/leaflet/leaflet.locationfilter.js
Merge remote-tracking branch 'openstreetmap/pull/1449'
[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.Layer.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         buttonPosition: 'topleft'
105     },
106
107     initialize: function(options) {
108         L.Util.setOptions(this, options);
109     },
110
111     addTo: function(map) {
112         map.addLayer(this);
113         return this;
114     },
115
116     onAdd: function(map) {
117         this._map = map;
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         this._layer = new L.LayerGroup();
289
290         // Calculate filter bounds
291         this._calculateBounds();
292
293         // Create rectangles
294         this._northRect = this._drawRectangle(this._northBounds);
295         this._westRect = this._drawRectangle(this._westBounds);
296         this._eastRect = this._drawRectangle(this._eastBounds);
297         this._southRect = this._drawRectangle(this._southBounds);
298         this._innerRect = this._drawRectangle(this.getBounds(), {
299             fillOpacity: 0,
300             stroke: true,
301             color: "white",
302             weight: 1,
303             opacity: 0.9
304         });
305
306         // Create resize markers
307         this._nwMarker = this._drawResizeMarker(this._nw);
308         this._neMarker = this._drawResizeMarker(this._ne);
309         this._swMarker = this._drawResizeMarker(this._sw);
310         this._seMarker = this._drawResizeMarker(this._se);
311
312         // Setup tracking of resize markers. Each marker has pair of
313         // follower markers that must be moved whenever the marker is
314         // moved. For example, whenever the north west resize marker
315         // moves, the south west marker must move along on the x-axis
316         // and the north east marker must move on the y axis
317         this._setupResizeMarkerTracking(this._nwMarker, {moveAlong: {lat: this._neMarker, lng: this._swMarker}});
318         this._setupResizeMarkerTracking(this._neMarker, {moveAlong: {lat: this._nwMarker, lng: this._seMarker}});
319         this._setupResizeMarkerTracking(this._swMarker, {moveAlong: {lat: this._seMarker, lng: this._nwMarker}});
320         this._setupResizeMarkerTracking(this._seMarker, {moveAlong: {lat: this._swMarker, lng: this._neMarker}});
321
322         // Create move marker
323         this._moveMarker = this._drawMoveMarker(this._nw);
324
325         this._initialDrawCalled = true;
326     },
327
328     /* Reposition all rectangles and markers to the current filter bounds. */    
329     _draw: function(options) {
330         options = L.Util.extend({repositionResizeMarkers: true}, options);
331
332         // Calculate filter bounds
333         this._calculateBounds();
334
335         // Reposition rectangles
336         this._northRect.setBounds(this._northBounds);
337         this._westRect.setBounds(this._westBounds);
338         this._eastRect.setBounds(this._eastBounds);
339         this._southRect.setBounds(this._southBounds);
340         this._innerRect.setBounds(this.getBounds());
341
342         // Reposition resize markers
343         if (options.repositionResizeMarkers) {
344             this._nwMarker.setLatLng(this._nw);
345             this._neMarker.setLatLng(this._ne);
346             this._swMarker.setLatLng(this._sw);
347             this._seMarker.setLatLng(this._se);
348         }
349
350         // Reposition the move marker
351         this._moveMarker.setLatLng(this._nw);
352     }, 
353
354     /* Adjust the location filter to the current map bounds */
355     _adjustToMap: function() {
356         this.setBounds(this._map.getBounds());
357         this._map.zoomOut();
358     },
359
360     /* Enable the location filter */
361     enable: function() {
362         if (this._enabled) {
363             return;
364         }
365
366         // Initialize corners
367         var bounds;
368         if (this._sw && this._ne) {
369             bounds = new L.LatLngBounds(this._sw, this._ne);
370         } else if (this.options.bounds) {
371             bounds = this.options.bounds;
372         } else {
373             bounds = this._map.getBounds();
374         }
375         this._map.invalidateSize();
376         this._nw = bounds.getNorthWest();
377         this._ne = bounds.getNorthEast();
378         this._sw = bounds.getSouthWest();
379         this._se = bounds.getSouthEast();
380             
381
382         // Update buttons
383         if (this._buttonContainer) {
384             this._buttonContainer.addClass("enabled");
385         }
386
387         if (this._enableButton) {
388             this._enableButton.setText(this.options.enableButton.disableText);
389         }
390
391         if (this.options.adjustButton) {
392             this._createAdjustButton();
393         }
394         
395         // Draw filter
396         this._initialDraw();
397         this._draw();
398
399         // Set up map move event listener
400         var that = this;
401         this._moveHandler = function() {
402             that._draw();
403         };
404         this._map.on("move", this._moveHandler);
405
406         // Add the filter layer to the map
407         this._layer.addTo(this._map);
408         
409         // Zoom out the map if necessary
410         var mapBounds = this._map.getBounds();
411         bounds = new L.LatLngBounds(this._sw, this._ne).modify(this._map, 10);
412         if (!mapBounds.contains(bounds.getSouthWest()) || !mapBounds.contains(bounds.getNorthEast())) {
413             this._map.fitBounds(bounds);
414         }
415
416         this._enabled = true;
417         
418         // Fire the enabled event
419         this.fire("enabled");
420     },
421
422     /* Disable the location filter */
423     disable: function() {
424         if (!this._enabled) {
425             return;
426         }
427
428         // Update buttons
429         if (this._buttonContainer) {
430             this._buttonContainer.removeClass("enabled");
431         }
432
433         if (this._enableButton) {
434             this._enableButton.setText(this.options.enableButton.enableText);
435         }
436
437         if (this._adjustButton) {
438             this._adjustButton.remove();
439         }
440
441         // Remove event listener
442         this._map.off("move", this._moveHandler);
443
444         // Remove rectangle layer from map
445         this._map.removeLayer(this._layer);
446
447         this._enabled = false;
448
449         // Fire the disabled event
450         this.fire("disabled");
451     },
452
453     /* Create a button that allows the user to adjust the location
454        filter to the current zoom */
455     _createAdjustButton: function() {
456         var that = this;
457         this._adjustButton = new L.Control.Button({
458             className: "adjust-button",
459             text: this.options.adjustButton.text,
460             
461             onClick: function(event) {
462                 that._adjustToMap();
463                 that.fire("adjustToZoomClick");
464             }
465         }).addTo(this._buttonContainer);
466     },
467
468     /* Create the location filter button container and the button that
469        toggles the location filter */
470     _initializeButtonContainer: function() {
471         var that = this;
472         this._buttonContainer = new L.Control.ButtonContainer({
473             className: "location-filter button-container",
474             position: this.options.buttonPosition
475         });
476
477         if (this.options.enableButton) {
478             this._enableButton = new L.Control.Button({
479                 className: "enable-button",
480                 text: this.options.enableButton.enableText,
481
482                 onClick: function(event) {
483                     if (!that._enabled) {
484                         // Enable the location filter
485                         that.enable();
486                         that.fire("enableClick");
487                     } else {
488                         // Disable the location filter
489                         that.disable();
490                         that.fire("disableClick");
491                     }
492                 }
493             }).addTo(this._buttonContainer);
494         }
495
496         this._buttonContainer.addTo(this._map);
497     }
498 });