]> git.openstreetmap.org Git - rails.git/blob - vendor/assets/leaflet/leaflet.locationfilter.js
Merge pull request #3491 from tomhughes/legacy-trace
[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     options: {
95         enableButton: {
96             enableText: "Select area",
97             disableText: "Remove selection"
98         },
99         adjustButton: {
100             text: "Select area within current zoom"
101         },
102         buttonPosition: 'topleft'
103     },
104
105     initialize: function(options) {
106         L.Util.setOptions(this, options);
107     },
108
109     addTo: function(map) {
110         map.addLayer(this);
111         return this;
112     },
113
114     onAdd: function(map) {
115         this._map = map;
116
117         if (this.options.enableButton || this.options.adjustButton) {
118             this._initializeButtonContainer();
119         }
120
121         if (this.options.enable) {
122             this.enable();
123         }
124     },
125
126     onRemove: function(map) {
127         this.disable();
128         if (this._buttonContainer) {
129             this._buttonContainer.removeFrom(map);
130         }
131     },
132
133     /* Get the current filter bounds */
134     getBounds: function() { 
135         return new L.LatLngBounds(this._sw, this._ne); 
136     },
137
138     setBounds: function(bounds) {
139         this._nw = bounds.getNorthWest();
140         this._ne = bounds.getNorthEast();
141         this._sw = bounds.getSouthWest();
142         this._se = bounds.getSouthEast();
143         if (this.isEnabled()) {
144             this._draw();
145             this.fire("change", {bounds: bounds});
146         }
147     },
148
149     isEnabled: function() {
150         return this._enabled;
151     },
152
153     /* Draw a rectangle */
154     _drawRectangle: function(bounds, options) {
155         options = options || {};
156         var defaultOptions = {
157             stroke: false,
158             fill: true,
159             fillColor: "black",
160             fillOpacity: 0.3,
161             clickable: false
162         };
163         options = L.Util.extend(defaultOptions, options);
164         var rect = new L.Rectangle(bounds, options);
165         rect.addTo(this._layer);
166         return rect;
167     },
168
169     /* Draw a draggable marker */
170     _drawImageMarker: function(point, options) {
171         var marker = new L.Marker(point, {
172             icon: new L.DivIcon({
173                 iconAnchor: options.anchor,
174                 iconSize: options.size,
175                 className: options.className
176             }),
177             draggable: true
178         });
179         marker.addTo(this._layer);
180         return marker;
181     },
182
183     /* Draw a move marker. Sets up drag listener that updates the
184        filter corners and redraws the filter when the move marker is
185        moved */
186     _drawMoveMarker: function(point) {
187         var that = this;
188         this._moveMarker = this._drawImageMarker(point, {
189             "className": "location-filter move-marker",
190             "anchor": [-10, -10],
191             "size": [13,13]
192         });
193         this._moveMarker.on('drag', function(e) {
194             var markerPos = that._moveMarker.getLatLng(),
195                 latDelta = markerPos.lat-that._nw.lat,
196                 lngDelta = markerPos.lng-that._nw.lng;
197             that._nw = new L.LatLng(that._nw.lat+latDelta, that._nw.lng+lngDelta, true);
198             that._ne = new L.LatLng(that._ne.lat+latDelta, that._ne.lng+lngDelta, true);
199             that._sw = new L.LatLng(that._sw.lat+latDelta, that._sw.lng+lngDelta, true);
200             that._se = new L.LatLng(that._se.lat+latDelta, that._se.lng+lngDelta, true);
201             that._draw();
202         });
203         this._setupDragendListener(this._moveMarker);
204         return this._moveMarker;
205     },
206
207     /* Draw a resize marker */
208     _drawResizeMarker: function(point, latFollower, lngFollower, otherPos) {
209         return this._drawImageMarker(point, {
210             "className": "location-filter resize-marker",
211             "anchor": [7, 6],
212             "size": [13, 12] 
213         });
214     },
215
216     /* Track moving of the given resize marker and update the markers
217        given in options.moveAlong to match the position of the moved
218        marker. Update filter corners and redraw the filter */
219     _setupResizeMarkerTracking: function(marker, options) {
220         var that = this;
221         marker.on('drag', function(e) {
222             var curPosition = marker.getLatLng(),
223                 latMarker = options.moveAlong.lat,
224                 lngMarker = options.moveAlong.lng;
225             // Move follower markers when this marker is moved
226             latMarker.setLatLng(new L.LatLng(curPosition.lat, latMarker.getLatLng().lng, true));
227             lngMarker.setLatLng(new L.LatLng(lngMarker.getLatLng().lat, curPosition.lng, true));
228             // Sort marker positions in nw, ne, sw, se order
229             var corners = [that._nwMarker.getLatLng(), 
230                            that._neMarker.getLatLng(), 
231                            that._swMarker.getLatLng(), 
232                            that._seMarker.getLatLng()];
233             corners.sort(function(a, b) {
234                 if (a.lat != b.lat)
235                     return b.lat-a.lat;
236                 else
237                     return a.lng-b.lng;
238             });
239             // Update corner points and redraw everything except the resize markers
240             that._nw = corners[0];
241             that._ne = corners[1];
242             that._sw = corners[2];
243             that._se = corners[3];
244             that._draw({repositionResizeMarkers: false});
245         });
246         this._setupDragendListener(marker);
247     },
248
249     /* Emit a change event whenever dragend is triggered on the
250        given marker */
251     _setupDragendListener: function(marker) {
252         var that = this;
253         marker.on('dragend', function(e) {
254             that.fire("change", {bounds: that.getBounds()});
255         });
256     },
257
258     /* Create bounds for the mask rectangles and the location
259        filter rectangle */
260     _calculateBounds: function() {
261         var mapBounds = this._map.getBounds(),
262             outerBounds = new L.LatLngBounds(
263                 new L.LatLng(mapBounds.getSouthWest().lat-0.1,
264                              mapBounds.getSouthWest().lng-0.1, true),
265                 new L.LatLng(mapBounds.getNorthEast().lat+0.1,
266                              mapBounds.getNorthEast().lng+0.1, true)
267             );
268
269         // The south west and north east points of the mask */
270         this._osw = outerBounds.getSouthWest();
271         this._one = outerBounds.getNorthEast();
272
273         // Bounds for the mask rectangles
274         this._northBounds = new L.LatLngBounds(new L.LatLng(this._ne.lat, this._osw.lng, true), this._one);
275         this._westBounds = new L.LatLngBounds(new L.LatLng(this._sw.lat, this._osw.lng, true), this._nw);
276         this._eastBounds = new L.LatLngBounds(this._se, new L.LatLng(this._ne.lat, this._one.lng, true));
277         this._southBounds = new L.LatLngBounds(this._osw, new L.LatLng(this._sw.lat, this._one.lng, true));
278     },
279
280     /* Initializes rectangles and markers */
281     _initialDraw: function() {
282         if (this._initialDrawCalled) {
283             return;
284         }
285
286         this._layer = new L.LayerGroup();
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({
471             className: "location-filter button-container",
472             position: this.options.buttonPosition
473         });
474
475         if (this.options.enableButton) {
476             this._enableButton = new L.Control.Button({
477                 className: "enable-button",
478                 text: this.options.enableButton.enableText,
479
480                 onClick: function(event) {
481                     if (!that._enabled) {
482                         // Enable the location filter
483                         that.enable();
484                         that.fire("enableClick");
485                     } else {
486                         // Disable the location filter
487                         that.disable();
488                         that.fire("disableClick");
489                     }
490                 }
491             }).addTo(this._buttonContainer);
492         }
493
494         this._buttonContainer.addTo(this._map);
495     }
496 });