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({
94 includes: L.Mixin.Events,
98 enableText: "Select area",
99 disableText: "Remove selection"
102 text: "Select area within current zoom"
104 buttonPosition: 'topleft'
107 initialize: function(options) {
108 L.Util.setOptions(this, options);
111 addTo: function(map) {
116 onAdd: function(map) {
119 if (this.options.enableButton || this.options.adjustButton) {
120 this._initializeButtonContainer();
123 if (this.options.enable) {
128 onRemove: function(map) {
130 if (this._buttonContainer) {
131 this._buttonContainer.removeFrom(map);
135 /* Get the current filter bounds */
136 getBounds: function() {
137 return new L.LatLngBounds(this._sw, this._ne);
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()) {
147 this.fire("change", {bounds: bounds});
151 isEnabled: function() {
152 return this._enabled;
155 /* Draw a rectangle */
156 _drawRectangle: function(bounds, options) {
157 options = options || {};
158 var defaultOptions = {
165 options = L.Util.extend(defaultOptions, options);
166 var rect = new L.Rectangle(bounds, options);
167 rect.addTo(this._layer);
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
181 marker.addTo(this._layer);
185 /* Draw a move marker. Sets up drag listener that updates the
186 filter corners and redraws the filter when the move marker is
188 _drawMoveMarker: function(point) {
190 this._moveMarker = this._drawImageMarker(point, {
191 "className": "location-filter move-marker",
192 "anchor": [-10, -10],
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);
205 this._setupDragendListener(this._moveMarker);
206 return this._moveMarker;
209 /* Draw a resize marker */
210 _drawResizeMarker: function(point, latFollower, lngFollower, otherPos) {
211 return this._drawImageMarker(point, {
212 "className": "location-filter resize-marker",
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) {
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) {
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});
248 this._setupDragendListener(marker);
251 /* Emit a change event whenever dragend is triggered on the
253 _setupDragendListener: function(marker) {
255 marker.on('dragend', function(e) {
256 that.fire("change", {bounds: that.getBounds()});
260 /* Create bounds for the mask rectangles and the location
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)
271 // The south west and north east points of the mask */
272 this._osw = outerBounds.getSouthWest();
273 this._one = outerBounds.getNorthEast();
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));
282 /* Initializes rectangles and markers */
283 _initialDraw: function() {
284 if (this._initialDrawCalled) {
288 this._layer = new L.LayerGroup();
290 // Calculate filter bounds
291 this._calculateBounds();
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(), {
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);
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}});
322 // Create move marker
323 this._moveMarker = this._drawMoveMarker(this._nw);
325 this._initialDrawCalled = true;
328 /* Reposition all rectangles and markers to the current filter bounds. */
329 _draw: function(options) {
330 options = L.Util.extend({repositionResizeMarkers: true}, options);
332 // Calculate filter bounds
333 this._calculateBounds();
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());
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);
350 // Reposition the move marker
351 this._moveMarker.setLatLng(this._nw);
354 /* Adjust the location filter to the current map bounds */
355 _adjustToMap: function() {
356 this.setBounds(this._map.getBounds());
360 /* Enable the location filter */
366 // Initialize corners
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;
373 bounds = this._map.getBounds();
375 this._map.invalidateSize();
376 this._nw = bounds.getNorthWest();
377 this._ne = bounds.getNorthEast();
378 this._sw = bounds.getSouthWest();
379 this._se = bounds.getSouthEast();
383 if (this._buttonContainer) {
384 this._buttonContainer.addClass("enabled");
387 if (this._enableButton) {
388 this._enableButton.setText(this.options.enableButton.disableText);
391 if (this.options.adjustButton) {
392 this._createAdjustButton();
399 // Set up map move event listener
401 this._moveHandler = function() {
404 this._map.on("move", this._moveHandler);
406 // Add the filter layer to the map
407 this._layer.addTo(this._map);
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);
416 this._enabled = true;
418 // Fire the enabled event
419 this.fire("enabled");
422 /* Disable the location filter */
423 disable: function() {
424 if (!this._enabled) {
429 if (this._buttonContainer) {
430 this._buttonContainer.removeClass("enabled");
433 if (this._enableButton) {
434 this._enableButton.setText(this.options.enableButton.enableText);
437 if (this._adjustButton) {
438 this._adjustButton.remove();
441 // Remove event listener
442 this._map.off("move", this._moveHandler);
444 // Remove rectangle layer from map
445 this._map.removeLayer(this._layer);
447 this._enabled = false;
449 // Fire the disabled event
450 this.fire("disabled");
453 /* Create a button that allows the user to adjust the location
454 filter to the current zoom */
455 _createAdjustButton: function() {
457 this._adjustButton = new L.Control.Button({
458 className: "adjust-button",
459 text: this.options.adjustButton.text,
461 onClick: function(event) {
463 that.fire("adjustToZoomClick");
465 }).addTo(this._buttonContainer);
468 /* Create the location filter button container and the button that
469 toggles the location filter */
470 _initializeButtonContainer: function() {
472 this._buttonContainer = new L.Control.ButtonContainer({
473 className: "location-filter button-container",
474 position: this.options.buttonPosition
477 if (this.options.enableButton) {
478 this._enableButton = new L.Control.Button({
479 className: "enable-button",
480 text: this.options.enableButton.enableText,
482 onClick: function(event) {
483 if (!that._enabled) {
484 // Enable the location filter
486 that.fire("enableClick");
488 // Disable the location filter
490 that.fire("disableClick");
493 }).addTo(this._buttonContainer);
496 this._buttonContainer.addTo(this._map);