Merge pull request #5290 from tbarsballe/bboxinteraction

Add extent interaction
This commit is contained in:
Bart van den Eijnden
2016-08-27 09:08:28 +02:00
committed by GitHub
5 changed files with 747 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
---
layout: example.html
title: Extent Interaction
shortdesc: Using an Extent interaction to draw an extent.
docs: >
<p>This example shows how to use an <code>Extent</code> interaction to draw a modifiable extent.</p>
<p>Use <code>Shift+Drag</code> to draw an extent.
<code>Shift+Drag</code> on the corners or edges of the extent to resize it. <code>Shift+Click</code> off the extent to remove it.
</p>
tags: "Extent, interaction, box"
---
<div id="map" class="map"></div>

View File

@@ -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);
}
});

View File

@@ -2705,6 +2705,47 @@ olx.interaction.DrawOptions.prototype.freehandCondition;
olx.interaction.DrawOptions.prototype.wrapX; olx.interaction.DrawOptions.prototype.wrapX;
/**
* @typedef {{extent: (ol.Extent|undefined),
* boxStyle: (ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined),
* pointerStyle: (ol.style.Style|Array.<ol.style.Style>|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.style.Style>|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.style.Style>|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 {{ * @typedef {{
* features: (ol.Collection.<ol.Feature>|undefined), * features: (ol.Collection.<ol.Feature>|undefined),

View File

@@ -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<Array<ol.Coordinate>>} 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_));
};

View File

@@ -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]);
});
});
});