diff --git a/examples/extent-interaction.html b/examples/extent-interaction.html
new file mode 100644
index 0000000000..cce19e3890
--- /dev/null
+++ b/examples/extent-interaction.html
@@ -0,0 +1,12 @@
+---
+layout: example.html
+title: Extent Interaction
+shortdesc: Using an Extent interaction to draw an extent.
+docs: >
+
This example shows how to use an Extent interaction to draw a modifiable extent.
+ Use Shift+Drag to draw an extent.
+ Shift+Drag on the corners or edges of the extent to resize it. Shift+Click off the extent to remove it.
+
+tags: "Extent, interaction, box"
+---
+
diff --git a/examples/extent-interaction.js b/examples/extent-interaction.js
new file mode 100644
index 0000000000..11d5918275
--- /dev/null
+++ b/examples/extent-interaction.js
@@ -0,0 +1,49 @@
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.events.condition');
+goog.require('ol.format.GeoJSON');
+goog.require('ol.interaction.Extent');
+goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
+goog.require('ol.source.OSM');
+goog.require('ol.source.Vector');
+
+var vectorSource = new ol.source.Vector({
+ url: 'data/geojson/countries.geojson',
+ format: new ol.format.GeoJSON()
+});
+
+var map = new ol.Map({
+ layers: [
+ new ol.layer.Tile({
+ source: new ol.source.OSM()
+ }),
+ new ol.layer.Vector({
+ source: vectorSource
+ })
+ ],
+ renderer: 'canvas',
+ target: 'map',
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 2
+ })
+});
+
+var extent = new ol.interaction.Extent({
+ condition: ol.events.condition.platformModifierKeyOnly
+});
+map.addInteraction(extent);
+extent.setActive(false);
+
+//Enable interaction by holding shift
+this.addEventListener('keydown', function(event) {
+ if (event.keyCode == 16) {
+ extent.setActive(true);
+ }
+});
+this.addEventListener('keyup', function(event) {
+ if (event.keyCode == 16) {
+ extent.setActive(false);
+ }
+});
diff --git a/externs/olx.js b/externs/olx.js
index ea0dc65fa3..abc6dbea76 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -2705,6 +2705,47 @@ olx.interaction.DrawOptions.prototype.freehandCondition;
olx.interaction.DrawOptions.prototype.wrapX;
+/**
+ * @typedef {{extent: (ol.Extent|undefined),
+ * boxStyle: (ol.style.Style|Array.|ol.StyleFunction|undefined),
+ * pointerStyle: (ol.style.Style|Array.|ol.StyleFunction|undefined),
+ * wrapX: (boolean|undefined)}}
+ * @api
+ */
+olx.interaction.ExtentOptions;
+
+/**
+ * Initial extent. Defaults to no inital extent
+ * @type {ol.Extent|undefined}
+ * @api
+ */
+olx.interaction.ExtentOptions.prototype.extent;
+
+/**
+ * Style for the drawn extent box.
+ * Defaults to ol.style.Style.createDefaultEditing()[ol.geom.GeometryType.POLYGON]
+ * @type {ol.style.Style|Array.|ol.StyleFunction|undefined}
+ * @api
+ */
+olx.interaction.ExtentOptions.prototype.boxStyle;
+
+/**
+ * Style for the cursor used to draw the extent.
+ * Defaults to ol.style.Style.createDefaultEditing()[ol.geom.GeometryType.POINT]
+ * @type {ol.style.Style|Array.|ol.StyleFunction|undefined}
+ * @api
+ */
+olx.interaction.ExtentOptions.prototype.pointerStyle;
+
+/**
+ * Wrap the drawn extent across multiple maps in the X direction?
+ * Only affects visuals, not functionality. Defaults to false.
+ * @type {boolean|undefined}
+ * @api
+ */
+olx.interaction.ExtentOptions.prototype.wrapX;
+
+
/**
* @typedef {{
* features: (ol.Collection.|undefined),
diff --git a/src/ol/interaction/extent.js b/src/ol/interaction/extent.js
new file mode 100644
index 0000000000..fb6b1a6e6d
--- /dev/null
+++ b/src/ol/interaction/extent.js
@@ -0,0 +1,506 @@
+goog.provide('ol.interaction.Extent');
+goog.provide('ol.interaction.ExtentEvent');
+goog.provide('ol.interaction.ExtentEventType');
+
+goog.require('ol');
+goog.require('ol.Feature');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserPointerEvent');
+goog.require('ol.coordinate');
+goog.require('ol.events.Event');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.layer.Vector');
+goog.require('ol.source.Vector');
+goog.require('ol.style.Style');
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.ExtentEventType = {
+ /**
+ * Triggered after the extent is changed
+ * @event ol.interaction.ExtentEvent
+ * @api
+ */
+ EXTENTCHANGED: 'extentchanged'
+};
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.Extent} instances are instances of
+ * this type.
+ *
+ * @constructor
+ * @param {ol.Extent} extent the new extent
+ * @extends {ol.events.Event}
+ */
+ol.interaction.ExtentEvent = function(extent) {
+ ol.events.Event.call(this, ol.interaction.ExtentEventType.EXTENTCHANGED);
+
+ /**
+ * The current extent.
+ * @type {ol.Extent}
+ * @api
+ */
+ this.extent_ = extent;
+};
+
+ol.inherits(ol.interaction.ExtentEvent, ol.events.Event);
+
+/**
+ * @classdesc
+ * Allows the user to draw a vector box by clicking and dragging on the map.
+ * Once drawn, the vector box can be modified by dragging its vertices or edges.
+ * This interaction is only supported for mouse devices.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @fires ol.interaction.ExtentEvent
+ * @param {olx.interaction.ExtentOptions} opt_options Options.
+ * @api
+ */
+ol.interaction.Extent = function(opt_options) {
+
+ /**
+ * Extent of the drawn box
+ * @type {ol.Extent}
+ * @private
+ */
+ this.extent_ = null;
+
+ /**
+ * Handler for pointer move events
+ * @type {function (ol.Coordinate): ol.Extent|null}
+ * @private
+ */
+ this.pointerHandler_ = null;
+
+ /**
+ * Pixel threshold to snap to extent
+ * @type {number}
+ * @private
+ */
+ this.pixelTolerance_ = 10;
+
+ /**
+ * Last known pixel coordinate of the pointer
+ * @type {ol.Pixel}
+ * @private
+ */
+ this.lastPixel_ = null;
+
+ /**
+ * Is the pointer snapped to an extent vertex
+ * @type {boolean}
+ * @private
+ */
+ this.snappedToVertex_ = false;
+
+ /**
+ * Feature for displaying the visible extent
+ * @type {ol.Feature}
+ * @private
+ */
+ this.extentFeature_ = null;
+
+ /**
+ * Feature for displaying the visible pointer
+ * @type {ol.Feature}
+ * @private
+ */
+ this.vertexFeature_ = null;
+
+ if (!opt_options) {
+ opt_options = {};
+ }
+
+ if (opt_options.extent) {
+ this.setExtent(opt_options.extent);
+ }
+
+ /* Inherit ol.interaction.Pointer */
+ ol.interaction.Pointer.call(this, {
+ handleDownEvent: ol.interaction.Extent.handleDownEvent_,
+ handleDragEvent: ol.interaction.Extent.handleDragEvent_,
+ handleEvent: ol.interaction.Extent.handleEvent_,
+ handleUpEvent: ol.interaction.Extent.handleUpEvent_
+ });
+
+ /**
+ * Layer for the extentFeature
+ * @type {ol.layer.Vector}
+ * @private
+ */
+ this.extentOverlay_ = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ useSpatialIndex: false,
+ wrapX: !!opt_options.wrapX
+ }),
+ style: opt_options.boxStyle ? opt_options.boxStyle : ol.interaction.Extent.getDefaultExtentStyleFunction_(),
+ updateWhileAnimating: true,
+ updateWhileInteracting: true
+ });
+
+ /**
+ * Layer for the vertexFeature
+ * @type {ol.layer.Vector}
+ * @private
+ */
+ this.vertexOverlay_ = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ useSpatialIndex: false,
+ wrapX: !!opt_options.wrapX
+ }),
+ style: opt_options.pointerStyle ? opt_options.pointerStyle : ol.interaction.Extent.getDefaultPointerStyleFunction_(),
+ updateWhileAnimating: true,
+ updateWhileInteracting: true
+ });
+};
+
+ol.inherits(ol.interaction.Extent, ol.interaction.Pointer);
+
+/**
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Event.
+ * @return {boolean} Propagate event?
+ * @this {ol.interaction.Extent}
+ * @private
+ */
+ol.interaction.Extent.handleEvent_ = function(mapBrowserEvent) {
+ if (!(mapBrowserEvent instanceof ol.MapBrowserPointerEvent)) {
+ return true;
+ }
+ //display pointer (if not dragging)
+ if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERMOVE && !this.handlingDownUpSequence) {
+ this.handlePointerMove_(mapBrowserEvent);
+ }
+ //call pointer to determine up/down/drag
+ ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent);
+ //return false to stop propagation
+ return false;
+};
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Event handled?
+ * @this {ol.interaction.Extent}
+ * @private
+ */
+ol.interaction.Extent.handleDownEvent_ = function(mapBrowserEvent) {
+ var pixel = mapBrowserEvent.pixel;
+ var map = mapBrowserEvent.map;
+
+ var extent = this.getExtent();
+ var vertex = this.snapToVertex_(pixel, map);
+
+ //find the extent corner opposite the passed corner
+ var getOpposingPoint = function(point) {
+ var x_ = null;
+ var y_ = null;
+ if (point[0] == extent[0]) {
+ x_ = extent[2];
+ } else if (point[0] == extent[2]) {
+ x_ = extent[0];
+ }
+ if (point[1] == extent[1]) {
+ y_ = extent[3];
+ } else if (point[1] == extent[3]) {
+ y_ = extent[1];
+ }
+ if (x_ !== null && y_ !== null) {
+ return [x_, y_];
+ }
+ return null;
+ };
+ if (vertex && extent) {
+ var x = (vertex[0] == extent[0] || vertex[0] == extent[2]) ? vertex[0] : null;
+ var y = (vertex[1] == extent[1] || vertex[1] == extent[3]) ? vertex[1] : null;
+
+ //snap to point
+ if (x !== null && y !== null) {
+ this.pointerHandler_ = ol.interaction.Extent.getPointHandler_(getOpposingPoint(vertex));
+ //snap to edge
+ } else if (x !== null) {
+ this.pointerHandler_ = ol.interaction.Extent.getEdgeHandler_(
+ getOpposingPoint([x, extent[1]]),
+ getOpposingPoint([x, extent[3]])
+ );
+ } else if (y !== null) {
+ this.pointerHandler_ = ol.interaction.Extent.getEdgeHandler_(
+ getOpposingPoint([extent[0], y]),
+ getOpposingPoint([extent[2], y])
+ );
+ }
+ //no snap - new bbox
+ } else {
+ vertex = map.getCoordinateFromPixel(pixel);
+ this.setExtent([vertex[0], vertex[1], vertex[0], vertex[1]]);
+ this.pointerHandler_ = ol.interaction.Extent.getPointHandler_(vertex);
+ }
+ return true; //event handled; start downup sequence
+};
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Event handled?
+ * @this {ol.interaction.Extent}
+ * @private
+ */
+ol.interaction.Extent.handleDragEvent_ = function(mapBrowserEvent) {
+ this.lastPixel_ = mapBrowserEvent.pixel;
+ if (this.pointerHandler_) {
+ var pixelCoordinate = mapBrowserEvent.coordinate;
+ this.setExtent(this.pointerHandler_(pixelCoordinate));
+ this.createOrUpdatePointerFeature_(pixelCoordinate);
+ }
+ return true;
+};
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.Extent}
+ * @private
+ */
+ol.interaction.Extent.handleUpEvent_ = function(mapBrowserEvent) {
+ this.pointerHandler_ = null;
+ //If bbox is zero area, set to null;
+ var extent = this.getExtent();
+ if (!extent || ol.extent.getArea(extent) === 0) {
+ this.setExtent(null);
+ }
+ return false; //Stop handling downup sequence
+};
+
+/**
+ * Returns the default style for the drawn bbox
+ *
+ * @return {ol.StyleFunction} Default Extent style
+ * @private
+ */
+ol.interaction.Extent.getDefaultExtentStyleFunction_ = function() {
+ var style = ol.style.Style.createDefaultEditing();
+ return function(feature, resolution) {
+ return style[ol.geom.GeometryType.POLYGON];
+ };
+};
+
+/**
+ * Returns the default style for the pointer
+ *
+ * @return {ol.StyleFunction} Default pointer style
+ * @private
+ */
+ol.interaction.Extent.getDefaultPointerStyleFunction_ = function() {
+ var style = ol.style.Style.createDefaultEditing();
+ return function(feature, resolution) {
+ return style[ol.geom.GeometryType.POINT];
+ };
+};
+
+/**
+ * @param {ol.Coordinate} fixedPoint corner that will be unchanged in the new extent
+ * @returns {function (ol.Coordinate): ol.Extent} event handler
+ * @private
+ */
+ol.interaction.Extent.getPointHandler_ = function(fixedPoint) {
+ return function(point) {
+ return ol.extent.boundingExtent([fixedPoint, point]);
+ };
+};
+
+/**
+ * @param {ol.Coordinate} fixedP1 first corner that will be unchanged in the new extent
+ * @param {ol.Coordinate} fixedP2 second corner that will be unchanged in the new extent
+ * @returns {function (ol.Coordinate): ol.Extent|null} event handler
+ * @private
+ */
+ol.interaction.Extent.getEdgeHandler_ = function(fixedP1, fixedP2) {
+ if (fixedP1[0] == fixedP2[0]) {
+ return function(point) {
+ return ol.extent.boundingExtent([fixedP1, [point[0], fixedP2[1]]]);
+ };
+ } else if (fixedP1[1] == fixedP2[1]) {
+ return function(point) {
+ return ol.extent.boundingExtent([fixedP1, [fixedP2[0], point[1]]]);
+ };
+ } else {
+ return null;
+ }
+};
+
+/**
+ * @param {ol.Extent} extent extent
+ * @returns {Array>} extent line segments
+ * @private
+ */
+ol.interaction.Extent.getSegments_ = function(extent) {
+ return [
+ [[extent[0], extent[1]], [extent[0], extent[3]]],
+ [[extent[0], extent[3]], [extent[2], extent[3]]],
+ [[extent[2], extent[3]], [extent[2], extent[1]]],
+ [[extent[2], extent[1]], [extent[0], extent[1]]]
+ ];
+};
+
+/**
+ * @param {ol.Pixel} pixel cursor location
+ * @param {ol.Map} map map
+ * @returns {ol.Coordinate|null} snapped vertex on extent
+ * @private
+ */
+ol.interaction.Extent.prototype.snapToVertex_ = function(pixel, map) {
+ var pixelCoordinate = map.getCoordinateFromPixel(pixel);
+ var sortByDistance = function(a, b) {
+ return ol.coordinate.squaredDistanceToSegment(pixelCoordinate, a) -
+ ol.coordinate.squaredDistanceToSegment(pixelCoordinate, b);
+ };
+ var extent = this.getExtent();
+ if (extent) {
+ //convert extents to line segments and find the segment closest to pixelCoordinate
+ var segments = ol.interaction.Extent.getSegments_(extent);
+ segments.sort(sortByDistance);
+ var closestSegment = segments[0];
+
+ var vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
+ closestSegment));
+ var vertexPixel = map.getPixelFromCoordinate(vertex);
+
+ //if the distance is within tolerance, snap to the segment
+ if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <=
+ this.pixelTolerance_) {
+
+ //test if we should further snap to a vertex
+ var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
+ var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
+ var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
+ var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2);
+ var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
+ this.snappedToVertex_ = dist <= this.pixelTolerance_;
+ if (this.snappedToVertex_) {
+ vertex = squaredDist1 > squaredDist2 ?
+ closestSegment[1] : closestSegment[0];
+ }
+ return vertex;
+ }
+ }
+ return null;
+};
+
+/**
+ * @param {ol.MapBrowserEvent} mapBrowserEvent pointer move event
+ * @private
+ */
+ol.interaction.Extent.prototype.handlePointerMove_ = function(mapBrowserEvent) {
+ var pixel = mapBrowserEvent.pixel;
+ var map = mapBrowserEvent.map;
+
+ var vertex = this.snapToVertex_(pixel, map);
+ if (!vertex) {
+ vertex = map.getCoordinateFromPixel(pixel);
+ }
+ this.createOrUpdatePointerFeature_(vertex);
+};
+
+/**
+ * @param {ol.Extent} extent extent
+ * @returns {ol.Feature} extent as featrue
+ * @private
+ */
+ol.interaction.Extent.prototype.createOrUpdateExtentFeature_ = function(extent) {
+ var extentFeature = this.extentFeature_;
+
+ if (!extentFeature) {
+ if (!extent) {
+ extentFeature = new ol.Feature({});
+ } else {
+ extentFeature = new ol.Feature(ol.geom.Polygon.fromExtent(extent));
+ }
+ this.extentFeature_ = extentFeature;
+ this.extentOverlay_.getSource().addFeature(extentFeature);
+ } else {
+ if (!extent) {
+ extentFeature.setGeometry(undefined);
+ } else {
+ extentFeature.setGeometry(ol.geom.Polygon.fromExtent(extent));
+ }
+ }
+ return extentFeature;
+};
+
+/**
+ * @this {ol.interaction.Extent}
+ * @private
+ */
+ol.interaction.Extent.prototype.removeExtentFeature_ = function() {
+ var extentFeature = this.extentFeature_;
+ if (extentFeature) {
+ this.extentOverlay_.getSource().removeFeature(extentFeature);
+ this.extentFeature_ = null;
+ }
+};
+
+/**
+ * @param {ol.Coordinate} vertex location of feature
+ * @returns {ol.Feature} vertex as feature
+ * @private
+ */
+ol.interaction.Extent.prototype.createOrUpdatePointerFeature_ = function(vertex) {
+ var vertexFeature = this.vertexFeature_;
+ if (!vertexFeature) {
+ vertexFeature = new ol.Feature(new ol.geom.Point(vertex));
+ this.vertexFeature_ = vertexFeature;
+ this.vertexOverlay_.getSource().addFeature(vertexFeature);
+ } else {
+ var geometry = /** @type {ol.geom.Point} */ (vertexFeature.getGeometry());
+ geometry.setCoordinates(vertex);
+ }
+ return vertexFeature;
+};
+
+/**
+ * @private
+ */
+ol.interaction.Extent.prototype.removePointerFeature_ = function() {
+ var vertexFeature = this.vertexFeature_;
+ if (vertexFeature) {
+ this.vertexOverlay_.getSource().removeFeature(vertexFeature);
+ this.vertexFeature_ = null;
+ }
+};
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.Extent.prototype.setMap = function(map) {
+ this.extentOverlay_.setMap(map);
+ this.vertexOverlay_.setMap(map);
+ ol.interaction.Pointer.prototype.setMap.call(this, map);
+};
+
+/**
+ * Returns the current drawn extent in the view projection
+ *
+ * @return {ol.Extent} Drawn extent in the view projection.
+ * @api
+ */
+ol.interaction.Extent.prototype.getExtent = function() {
+ return this.extent_;
+};
+
+/**
+ * Manually sets the drawn extent, using the view projection.
+ *
+ * @param {ol.Extent} extent Extent
+ * @api
+ */
+ol.interaction.Extent.prototype.setExtent = function(extent) {
+ //Null extent means no bbox
+ this.extent_ = extent ? extent : null;
+ this.createOrUpdateExtentFeature_(extent);
+ this.dispatchEvent(new ol.interaction.ExtentEvent(this.extent_));
+};
diff --git a/test/spec/ol/interaction/extent.test.js b/test/spec/ol/interaction/extent.test.js
new file mode 100644
index 0000000000..38eba421e3
--- /dev/null
+++ b/test/spec/ol/interaction/extent.test.js
@@ -0,0 +1,139 @@
+goog.provide('ol.test.interaction.Extent');
+
+goog.require('ol.Map');
+goog.require('ol.MapBrowserPointerEvent');
+goog.require('ol.View');
+goog.require('ol.interaction.Extent');
+goog.require('ol.pointer.PointerEvent');
+
+describe('ol.interaction.Extent', function() {
+
+ var target, map, interaction;
+
+ var width = 360;
+ var height = 180;
+
+ beforeEach(function(done) {
+ target = document.createElement('div');
+
+ var style = target.style;
+ style.position = 'absolute';
+ style.left = '-1000px';
+ style.top = '-1000px';
+ style.width = width + 'px';
+ style.height = height + 'px';
+ document.body.appendChild(target);
+
+ map = new ol.Map({
+ target: target,
+ layers: [],
+ view: new ol.View({
+ projection: 'EPSG:4326',
+ center: [0, 0],
+ resolution: 1
+ })
+ });
+
+ map.once('postrender', function() {
+ done();
+ });
+
+ interaction = new ol.interaction.Extent();
+ map.addInteraction(interaction);
+ });
+
+ afterEach(function() {
+ map.dispose();
+ document.body.removeChild(target);
+ });
+
+ /**
+ * Simulates a browser event on the map viewport. The client x/y location
+ * will be adjusted as if the map were centered at 0,0.
+ * @param {string} type Event type.
+ * @param {number} x Horizontal offset from map center.
+ * @param {number} y Vertical offset from map center.
+ * @param {boolean=} opt_shiftKey Shift key is pressed.
+ * @param {number} button The mouse button.
+ */
+ function simulateEvent(type, x, y, opt_shiftKey, button) {
+ var viewport = map.getViewport();
+ // calculated in case body has top < 0 (test runner with small window)
+ var position = viewport.getBoundingClientRect();
+ var shiftKey = opt_shiftKey !== undefined ? opt_shiftKey : false;
+ var pointerEvent = new ol.pointer.PointerEvent(type, {
+ type: type,
+ button: button,
+ clientX: position.left + x + width / 2,
+ clientY: position.top - y + height / 2,
+ shiftKey: shiftKey
+ });
+ var event = new ol.MapBrowserPointerEvent(type, map, pointerEvent);
+ event.pointerEvent.pointerId = 1;
+ map.handleMapBrowserEvent(event);
+ }
+ describe('snap to vertex', function() {
+ it('snap to vertex works', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ expect(interaction.snapToVertex_([230,40], map)).to.eql([50,50]);
+ expect(interaction.snapToVertex_([231,41], map)).to.eql([50,50]);
+ });
+ it('snap to edge works', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ expect(interaction.snapToVertex_([230,90], map)).to.eql([50,0]);
+ expect(interaction.snapToVertex_([230,89], map)).to.eql([50,1]);
+ expect(interaction.snapToVertex_([231,90], map)).to.eql([50,0]);
+ });
+ });
+
+ describe('draw extent', function() {
+
+ it('drawing extent works', function() {
+ simulateEvent('pointerdown', -50, -50, false, 0);
+ simulateEvent('pointerdrag', 50, 50, false, 0);
+ simulateEvent('pointerup', 50, 50, false, 0);
+
+ expect(interaction.getExtent()).to.eql([-50,-50,50,50]);
+ });
+
+ it('clicking off extent nulls extent', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ simulateEvent('pointerdown', -10, -10, false, 0);
+ simulateEvent('pointerup', -10, -10, false, 0);
+
+ expect(interaction.getExtent()).to.equal(null);
+ });
+
+ it('clicking on extent does not null extent', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ simulateEvent('pointerdown', 50, 50, false, 0);
+ simulateEvent('pointerup', 50, 50, false, 0);
+
+ expect(interaction.getExtent()).to.eql([-50,-50,50,50]);
+ });
+
+ it('snap and drag vertex works', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ simulateEvent('pointerdown', 51, 49, false, 0);
+ simulateEvent('pointerdrag', -70, -40, false, 0);
+ simulateEvent('pointerup', -70, -40, false, 0);
+
+ expect(interaction.getExtent()).to.eql([-70,-50,-50,-40]);
+ });
+
+ it('snap and drag edge works', function() {
+ interaction.setExtent([-50,-50,50,50]);
+
+ simulateEvent('pointerdown', 51, 5, false, 0);
+ simulateEvent('pointerdrag', 20, -30, false, 0);
+ simulateEvent('pointerup', 20, -30, false, 0);
+
+ expect(interaction.getExtent()).to.eql([-50,-50,20,50]);
+ });
+ });
+});