2 * Leaflet.locationfilter - leaflet location filter plugin
3 * Copyright (C) 2012, Tripbirds.com
6 * Licensed under the MIT License.
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);
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));
20 return new L.LatLngBounds(sw, ne);
23 L.Control.Button = L.Class.extend({
24 initialize: function(options) {
25 L.Util.setOptions(this, options);
28 addTo: function(container) {
29 container.addButton(this);
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);
40 this._onClick = function(event) {
41 that.options.onClick.call(that, event);
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);
53 L.DomEvent.off(this._button, "click", this._onClick);
54 this._buttonContainer.getContainer().removeChild(this._button);
57 setText: function(text) {
58 this._button.title = text;
59 this._button.innerHTML = text;
63 L.Control.ButtonContainer = L.Control.extend({
68 getContainer: function() {
69 if (!this._container) {
70 this._container = L.DomUtil.create('div', this.options.className);
72 return this._container;
75 onAdd: function (map) {
77 return this.getContainer();
80 addButton: function(button) {
84 addClass: function(className) {
85 L.DomUtil.addClass(this.getContainer(), className);
88 removeClass: function(className) {
89 L.DomUtil.removeClass(this.getContainer(), className);
93 L.LocationFilter = L.Layer.extend({
96 enableText: "Select area",
97 disableText: "Remove selection"
100 text: "Select area within current zoom"
102 buttonPosition: 'topleft'
105 initialize: function(options) {
106 L.Util.setOptions(this, options);
109 addTo: function(map) {
114 onAdd: function(map) {
117 if (this.options.enableButton || this.options.adjustButton) {
118 this._initializeButtonContainer();
121 if (this.options.enable) {
126 onRemove: function(map) {
128 if (this._buttonContainer) {
129 this._buttonContainer.removeFrom(map);
133 /* Get the current filter bounds */
134 getBounds: function() {
135 return new L.LatLngBounds(this._sw, this._ne);
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()) {
145 this.fire("change", {bounds: bounds});
149 isEnabled: function() {
150 return this._enabled;
153 /* Draw a rectangle */
154 _drawRectangle: function(bounds, options) {
155 options = options || {};
156 var defaultOptions = {
163 options = L.Util.extend(defaultOptions, options);
164 var rect = new L.Rectangle(bounds, options);
165 rect.addTo(this._layer);
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
179 marker.addTo(this._layer);
183 /* Draw a move marker. Sets up drag listener that updates the
184 filter corners and redraws the filter when the move marker is
186 _drawMoveMarker: function(point) {
188 this._moveMarker = this._drawImageMarker(point, {
189 "className": "location-filter move-marker",
190 "anchor": [-10, -10],
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);
203 this._setupDragendListener(this._moveMarker);
204 return this._moveMarker;
207 /* Draw a resize marker */
208 _drawResizeMarker: function(point, latFollower, lngFollower, otherPos) {
209 return this._drawImageMarker(point, {
210 "className": "location-filter resize-marker",
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) {
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) {
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});
246 this._setupDragendListener(marker);
249 /* Emit a change event whenever dragend is triggered on the
251 _setupDragendListener: function(marker) {
253 marker.on('dragend', function(e) {
254 that.fire("change", {bounds: that.getBounds()});
258 /* Create bounds for the mask rectangles and the location
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)
269 // The south west and north east points of the mask */
270 this._osw = outerBounds.getSouthWest();
271 this._one = outerBounds.getNorthEast();
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));
280 /* Initializes rectangles and markers */
281 _initialDraw: function() {
282 if (this._initialDrawCalled) {
286 this._layer = new L.LayerGroup();
288 // Calculate filter bounds
289 this._calculateBounds();
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(), {
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);
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}});
320 // Create move marker
321 this._moveMarker = this._drawMoveMarker(this._nw);
323 this._initialDrawCalled = true;
326 /* Reposition all rectangles and markers to the current filter bounds. */
327 _draw: function(options) {
328 options = L.Util.extend({repositionResizeMarkers: true}, options);
330 // Calculate filter bounds
331 this._calculateBounds();
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());
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);
348 // Reposition the move marker
349 this._moveMarker.setLatLng(this._nw);
352 /* Adjust the location filter to the current map bounds */
353 _adjustToMap: function() {
354 this.setBounds(this._map.getBounds());
358 /* Enable the location filter */
364 // Initialize corners
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;
371 bounds = this._map.getBounds();
373 this._map.invalidateSize();
374 this._nw = bounds.getNorthWest();
375 this._ne = bounds.getNorthEast();
376 this._sw = bounds.getSouthWest();
377 this._se = bounds.getSouthEast();
381 if (this._buttonContainer) {
382 this._buttonContainer.addClass("enabled");
385 if (this._enableButton) {
386 this._enableButton.setText(this.options.enableButton.disableText);
389 if (this.options.adjustButton) {
390 this._createAdjustButton();
397 // Set up map move event listener
399 this._moveHandler = function() {
402 this._map.on("move", this._moveHandler);
404 // Add the filter layer to the map
405 this._layer.addTo(this._map);
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);
414 this._enabled = true;
416 // Fire the enabled event
417 this.fire("enabled");
420 /* Disable the location filter */
421 disable: function() {
422 if (!this._enabled) {
427 if (this._buttonContainer) {
428 this._buttonContainer.removeClass("enabled");
431 if (this._enableButton) {
432 this._enableButton.setText(this.options.enableButton.enableText);
435 if (this._adjustButton) {
436 this._adjustButton.remove();
439 // Remove event listener
440 this._map.off("move", this._moveHandler);
442 // Remove rectangle layer from map
443 this._map.removeLayer(this._layer);
445 this._enabled = false;
447 // Fire the disabled event
448 this.fire("disabled");
451 /* Create a button that allows the user to adjust the location
452 filter to the current zoom */
453 _createAdjustButton: function() {
455 this._adjustButton = new L.Control.Button({
456 className: "adjust-button",
457 text: this.options.adjustButton.text,
459 onClick: function(event) {
461 that.fire("adjustToZoomClick");
463 }).addTo(this._buttonContainer);
466 /* Create the location filter button container and the button that
467 toggles the location filter */
468 _initializeButtonContainer: function() {
470 this._buttonContainer = new L.Control.ButtonContainer({
471 className: "location-filter button-container",
472 position: this.options.buttonPosition
475 if (this.options.enableButton) {
476 this._enableButton = new L.Control.Button({
477 className: "enable-button",
478 text: this.options.enableButton.enableText,
480 onClick: function(event) {
481 if (!that._enabled) {
482 // Enable the location filter
484 that.fire("enableClick");
486 // Disable the location filter
488 that.fire("disableClick");
491 }).addTo(this._buttonContainer);
494 this._buttonContainer.addTo(this._map);