diff --git a/src/event.js b/src/event.js
index c32385857e..cc036c0612 100644
--- a/src/event.js
+++ b/src/event.js
@@ -197,6 +197,33 @@ geo_event.brushend = 'geo_brushend';
 //////////////////////////////////////////////////////////////////////////////
 geo_event.brushstart = 'geo_brushstart';
 
+//////////////////////////////////////////////////////////////////////////////
+/**
+ * Triggered after a selection ends.
+ * The event object extends {@link geo.brushSelection}.
+ * @mixes geo.brushSelection
+ */
+//////////////////////////////////////////////////////////////////////////////
+geo_event.select = 'geo_select';
+
+//////////////////////////////////////////////////////////////////////////////
+/**
+ * Triggered after a zoom selection ends.
+ * The event object extends {@link geo.brushSelection}.
+ * @mixes geo.brushSelection
+ */
+//////////////////////////////////////////////////////////////////////////////
+geo_event.zoomselect = 'geo_zoomselect';
+
+//////////////////////////////////////////////////////////////////////////////
+/**
+ * Triggered after an unzoom selection ends.
+ * The event object extends {@link geo.brushSelection}.
+ * @mixes geo.brushSelection
+ */
+//////////////////////////////////////////////////////////////////////////////
+geo_event.unzoomselect = 'geo_unzoomselect';
+
 //////////////////////////////////////////////////////////////////////////////
 /**
  * Triggered before a map navigation animation begins.  Set
diff --git a/src/mapInteractor.js b/src/mapInteractor.js
index 727e785056..84bb33a3b2 100644
--- a/src/mapInteractor.js
+++ b/src/mapInteractor.js
@@ -68,19 +68,23 @@ var mapInteractor = function (args) {
       zoomMoveButton: 'right',
       zoomMoveModifiers: {},
       rotateMoveButton: 'left',
-      rotateMoveModifiers: {'ctrl': true},
+      rotateMoveModifiers: {ctrl: true},
       panWheelEnabled: false,
       panWheelModifiers: {},
       zoomWheelEnabled: true,
       zoomWheelModifiers: {},
       rotateWheelEnabled: true,
-      rotateWheelModifiers: {'ctrl': true},
+      rotateWheelModifiers: {ctrl: true},
       wheelScaleX: 1,
       wheelScaleY: 1,
       zoomScale: 1,
       rotateWheelScale: 6 * Math.PI / 180,
       selectionButton: 'left',
-      selectionModifiers: {'shift': true},
+      selectionModifiers: {shift: true, ctrl: true},
+      zoomSelectionButton: 'left',
+      zoomSelectionModifiers: {shift: true},
+      unzoomSelectionButton: 'right',
+      unzoomSelectionModifiers: {shift: true},
       momentum: {
         enabled: true,
         maxSpeed: 2.5,
@@ -202,7 +206,7 @@ var mapInteractor = function (args) {
   //   // a mousemove.
   //   click: {
   //     enabled: true | false,
-  //     buttons: {'left': true, 'right': true, 'middle': true}
+  //     buttons: {left: true, right: true, middle: true}
   //     duration: 0,
   //     cancelOnMove: true // cancels click if the mouse is moved before release
   //   }
@@ -406,7 +410,9 @@ var mapInteractor = function (args) {
     if (m_options.panMoveButton === 'right' ||
         m_options.zoomMoveButton === 'right' ||
         m_options.rotateMoveButton === 'right' ||
-        m_options.selectionButton === 'right') {
+        m_options.selectionButton === 'right' ||
+        m_options.zoomSelectionButton === 'right' ||
+        m_options.unzoomSelectionButton === 'right') {
       $node.on('contextmenu.geojs', function () { return false; });
     }
     return m_this;
@@ -644,6 +650,10 @@ var mapInteractor = function (args) {
       action = 'rotate';
     } else if (eventMatch(m_options.selectionButton, m_options.selectionModifiers)) {
       action = 'select';
+    } else if (eventMatch(m_options.zoomSelectionButton, m_options.zoomSelectionModifiers)) {
+      action = 'zoomselect';
+    } else if (eventMatch(m_options.unzoomSelectionButton, m_options.unzoomSelectionModifiers)) {
+      action = 'unzoomselect';
     }
 
     // cancel transitions and momentum on click
@@ -668,7 +678,7 @@ var mapInteractor = function (args) {
         delta: {x: 0, y: 0}
       };
 
-      if (action === 'select') {
+      if (action === 'select' || action === 'zoomselect' || action === 'unzoomselect') {
         // Make sure the old selection layer is gone.
         if (m_selectionLayer) {
           m_selectionLayer.clear();
@@ -789,7 +799,7 @@ var mapInteractor = function (args) {
       cx = m_mouse.map.x - m_this.map().size().width / 2;
       cy = m_mouse.map.y - m_this.map().size().height / 2;
       m_this.map().rotation(m_state.origin.rotation + Math.atan2(cy, cx));
-    } else if (m_state.action === 'select') {
+    } else if (m_state.action === 'select' || m_state.action === 'zoomselect' || m_state.action === 'unzoomselect') {
       // Get the bounds of the current selection
       selectionObj = m_this._getSelection();
       m_this.map().geoTrigger(geo_event.brush, selectionObj);
@@ -876,6 +886,71 @@ var mapInteractor = function (args) {
     };
   }
 
+  ////////////////////////////////////////////////////////////////////////////
+  /**
+   * Based on the screen coodinates of a selection, zoom or unzoom and
+   * recenter.
+   *
+   * @private
+   * @param {string} action Either 'zoomselect' or 'unzoomselect'.
+   * @param {object} lowerLeft the x and y coordinates of the lower left corner
+   *    of the zoom rectangle.
+   * @param {object} upperRight the x and y coordinates of the upper right
+   *    corner of the zoom rectangle.
+   */
+  ////////////////////////////////////////////////////////////////////////////
+  this._zoomFromSelection = function (action, lowerLeft, upperRight) {
+    if (action !== 'zoomselect' && action !== 'unzoomselect') {
+      return;
+    }
+    if (lowerLeft.x === upperRight.x || lowerLeft.y === upperRight.y) {
+      return;
+    }
+    var zoom, center,
+        map = m_this.map(),
+        mapsize = map.size();
+    /* To arbitrarily handle rotation and projection, we center the map at the
+     * central coordinate of the selection and set the zoom level such that the
+     * four corners are just barely on the map.  When unzooming (zooming out),
+     * we ensure that the previous view is centered in the selection but use
+     * the maximal size for the zoom factor. */
+    var scaling = {
+      x: Math.abs((upperRight.x - lowerLeft.x) / mapsize.width),
+      y: Math.abs((upperRight.y - lowerLeft.y) / mapsize.height)
+    };
+    if (action === 'zoomselect') {
+      center = map.displayToGcs({
+        x: (lowerLeft.x + upperRight.x) / 2,
+        y: (lowerLeft.y + upperRight.y) / 2
+      }, null);
+      zoom = map.zoom() - Math.log2(Math.max(scaling.x, scaling.y));
+    } else {  /* unzoom */
+      /* To make the existing visible map entirely within the selection
+       * rectangle, this would be changed to Math.min instead of Math.max of
+       * the scaling factors.  This felt wrong, though. */
+      zoom = map.zoom() + Math.log2(Math.max(scaling.x, scaling.y));
+      /* Record the current center.  Later, this is panned to the center of the
+       * selection rectangle. */
+      center = map.center(undefined, null);
+    }
+    /* When discrete zoom is enable, always round down.  We have to do this
+     * explicitly, as otherwise we may zoom too far and the selection will not
+     * be completely visible. */
+    if (map.discreteZoom()) {
+      zoom = Math.floor(zoom);
+    }
+    map.zoom(zoom);
+    if (action === 'zoomselect') {
+      map.center(center, null);
+    } else {
+      var newcenter = map.gcsToDisplay(center, null);
+      map.pan({
+        x: (lowerLeft.x + upperRight.x) / 2 - newcenter.x,
+        y: (lowerLeft.y + upperRight.y) / 2 - newcenter.y
+      });
+    }
+  };
+
   ////////////////////////////////////////////////////////////////////////////
   /**
    * Handle event when a mouse button is unpressed on the document.
@@ -903,7 +978,8 @@ var mapInteractor = function (args) {
       evt.preventDefault();
     }
 
-    if (m_state.action === 'select') {
+    if (m_state.action === 'select' || m_state.action === 'zoomselect' || m_state.action === 'unzoomselect') {
+      m_this._getMousePosition(evt);
       selectionObj = m_this._getSelection();
 
       m_selectionLayer.clear();
@@ -912,6 +988,9 @@ var mapInteractor = function (args) {
       m_selectionQuad = null;
 
       m_this.map().geoTrigger(geo_event.brushend, selectionObj);
+      m_this.map().geoTrigger(geo_event[m_state.action], selectionObj);
+      m_this._zoomFromSelection(m_state.action, selectionObj.display.lowerLeft,
+                                selectionObj.display.upperRight);
     }
 
     // reset the interactor state
@@ -1410,6 +1489,9 @@ var mapInteractor = function (args) {
         }
       }
     );
+    if (type.indexOf('.geojs') >= 0) {
+      $(document).trigger(evt);
+    }
     $node.trigger(evt);
   };
   this._connectEvents();
diff --git a/src/renderer.js b/src/renderer.js
index 086c406455..44416bd455 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -62,17 +62,6 @@ var renderer = function (arg) {
     }
   };
 
-  ////////////////////////////////////////////////////////////////////////////
-  /**
-   * Get base layer that belongs to this renderer
-   */
-  ////////////////////////////////////////////////////////////////////////////
-  this.baseLayer = function () {
-    if (m_this.map()) {
-      return m_this.map().baseLayer();
-    }
-  };
-
   ////////////////////////////////////////////////////////////////////////////
   /**
    * Get/Set if renderer has been initialized
diff --git a/tests/cases/mapInteractor.js b/tests/cases/mapInteractor.js
index 58fad62a6b..e7ce75cab6 100644
--- a/tests/cases/mapInteractor.js
+++ b/tests/cases/mapInteractor.js
@@ -36,11 +36,12 @@ describe('mapInteractor', function () {
   function mockedMap(node) {
 
     var map = geo.object();
-    var base = geo.object();
+    var voidfunc = function () {};
     var info = {
       pan: 0,
       zoom: 0,
       rotation: 0,
+      centerCalls: 0,
       rotationArgs: {},
       panArgs: {},
       zoomArgs: {},
@@ -48,19 +49,18 @@ describe('mapInteractor', function () {
     };
 
     map.node = function () { return $(node); };
-    base.displayToGcs = function (val) {
+    map.displayToGcs = function (val) {
       return {
         x: val.x - info.center.x - $(node).width() / 2,
         y: val.y - info.center.y - $(node).height() / 2
       };
     };
-    base.gcsToDisplay = function (val) {
+    map.gcsToDisplay = function (val) {
       return {
         x: val.x + info.center.x + $(node).width() / 2,
         y: val.y + info.center.y + $(node).height() / 2
       };
     };
-    map.baseLayer = function () { return base; };
     map.zoom = function (arg) {
       if (arg === undefined) {
         return 2;
@@ -83,8 +83,14 @@ describe('mapInteractor', function () {
       info.center.x += info.panArgs.x || 0;
       info.center.y += info.panArgs.y || 0;
     };
-    map.center = function () {
-      return {x: info.center.x, y: info.center.y};
+    map.center = function (arg) {
+      if (arg === undefined) {
+        return {x: info.center.x, y: info.center.y};
+      }
+      info.center.x = arg.x;
+      info.center.y = arg.y;
+      info.centerCalls += 1;
+      info.centerArgs = arg;
     };
     map.size = function () {
       return {width: 100, height: 100};
@@ -106,9 +112,27 @@ describe('mapInteractor', function () {
     map.maxBounds = function () {
       return {left: -200, top: 200, right: 200, bottom: -200};
     };
-    map.displayToGcs = base.displayToGcs;
-    map.gcsToDisplay = base.gcsToDisplay;
     map.info = info;
+
+    map.createLayer = function () {
+      var layer = geo.object();
+      layer.createFeature = function () {
+        var feature = geo.object();
+        feature.style = voidfunc;
+        feature.data = voidfunc;
+        feature.draw = voidfunc;
+        return feature;
+      };
+      layer.clear = voidfunc;
+      return layer;
+    };
+    map.deleteLayer = voidfunc;
+    map.discreteZoom = function (arg) {
+      if (arg === undefined) {
+        return map.info.discreteZoom;
+      }
+      map.info.discreteZoom = arg;
+    };
     return map;
   }
 
@@ -426,7 +450,7 @@ describe('mapInteractor', function () {
       zoomMoveButton: null,
       zoomWheelEnabled: false,
       rotateMoveButton: 'left',
-      rotateMoveModifiers: {'ctrl': false},
+      rotateMoveModifiers: {ctrl: false},
       rotateWheelEnabled: false,
       throttle: false
     });
@@ -469,6 +493,146 @@ describe('mapInteractor', function () {
         0.1 - Math.atan2(20 - 50, 20 - 50) + Math.atan2(25 - 50, 30 - 50));
   });
 
+  it('Test zoom selection event propagation', function () {
+    var map = mockedMap('#mapNode1');
+
+    var interactor = geo.mapInteractor({
+      map: map,
+      panMoveButton: null,
+      panWheelEnabled: false,
+      zoomMoveButton: null,
+      zoomWheelEnabled: false,
+      rotateMoveButton: null,
+      rotateWheelEnabled: false,
+      zoomSelectionButton: 'left',
+      zoomSelectionModifiers: {shift: false},
+      unzoomSelectionButton: 'middle',
+      unzoomSelectionModifiers: {shift: false},
+      throttle: false
+    });
+
+    // initialize the selection
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mousemove', {map: {x: 30, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mouseup.geojs', {map: {x: 40, y: 50}, button: 'left'}
+    );
+
+    // check the selection event was called
+    expect(map.info.zoom).toBe(1);
+    expect(map.info.zoomArgs).toBeCloseTo(3.75, 1);
+    expect(map.info.centerCalls).toBe(1);
+    expect(map.info.centerArgs.x).toBeCloseTo(-370);
+    expect(map.info.centerArgs.y).toBeCloseTo(35);
+
+    map.discreteZoom(true);
+
+    // start with an unzoom, but switch to a zoom
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'middle'}
+    );
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mouseup.geojs', {map: {x: 0, y: -30}, button: 'left'}
+    );
+    expect(map.info.zoom).toBe(2);
+    expect(map.info.zoomArgs).toBe(3);
+    expect(map.info.centerCalls).toBe(2);
+    expect(map.info.centerArgs.x).toBeCloseTo(-20);
+    expect(map.info.centerArgs.y).toBeCloseTo(-40);
+
+    /* If tehre is no movement, nothing should happen */
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mouseup.geojs', {map: {x: 20, y: 20}, button: 'left'}
+    );
+    expect(map.info.zoom).toBe(2);
+  });
+
+  it('Test unzoom selection event propagation', function () {
+    var map = mockedMap('#mapNode1');
+
+    var interactor = geo.mapInteractor({
+      map: map,
+      panMoveButton: null,
+      panWheelEnabled: false,
+      zoomMoveButton: null,
+      zoomWheelEnabled: false,
+      rotateMoveButton: null,
+      rotateWheelEnabled: false,
+      zoomSelectionButton: 'left',
+      zoomSelectionModifiers: {shift: false},
+      unzoomSelectionButton: 'middle',
+      unzoomSelectionModifiers: {shift: false},
+      throttle: false
+    });
+
+    // initialize the selection
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'middle'}
+    );
+    interactor.simulateEvent(
+      'mousemove', {map: {x: 30, y: 20}, button: 'middle'}
+    );
+    interactor.simulateEvent(
+      'mouseup.geojs', {map: {x: 40, y: 50}, button: 'middle'}
+    );
+
+    // check the selection event was called
+    expect(map.info.zoom).toBe(1);
+    expect(map.info.zoomArgs).toBeCloseTo(0.25, 1);
+    expect(map.info.centerCalls).toBe(0);
+    expect(map.info.pan).toBe(1);
+    expect(map.info.panArgs.x).toBeCloseTo(-370);
+    expect(map.info.panArgs.y).toBeCloseTo(35);
+  });
+
+  it('Test selection event propagation', function () {
+    var map = mockedMap('#mapNode1'),
+        triggered = 0;
+
+    var interactor = geo.mapInteractor({
+      map: map,
+      panMoveButton: null,
+      panWheelEnabled: false,
+      zoomMoveButton: null,
+      zoomWheelEnabled: false,
+      rotateMoveButton: null,
+      rotateWheelEnabled: false,
+      selectionButton: 'left',
+      selectionModifiers: {shift: false, ctrl: false},
+      throttle: false
+    });
+    map.geoOn(geo.event.select, function () {
+      triggered += 1;
+    });
+
+    // initialize the selection
+    interactor.simulateEvent(
+      'mousedown', {map: {x: 20, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mousemove', {map: {x: 30, y: 20}, button: 'left'}
+    );
+    interactor.simulateEvent(
+      'mouseup.geojs', {map: {x: 40, y: 50}, button: 'left'}
+    );
+
+    // check the selection event was called
+    expect(map.info.zoom).toBe(0);
+    expect(map.info.centerCalls).toBe(0);
+    expect(map.info.pan).toBe(0);
+    expect(triggered).toBe(1);
+  });
+
   describe('pause state', function () {
     it('defaults', function () {
       expect(geo.mapInteractor().pause()).toBe(false);