diff --git a/externs/olx.js b/externs/olx.js index 96c3b2648f..cf5ba38d6e 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -2504,6 +2504,22 @@ olx.interaction.DrawOptions.prototype.freehandCondition; olx.interaction.DrawOptions.prototype.wrapX; +/** + * @typedef {{features: (ol.Collection.|undefined)}} + * @api + */ +olx.interaction.TranslateOptions; + + +/** + * Only features contained in this collection will be able to be translated. If + * not specified, all features on the map will be able to be translated. + * @type {ol.Collection.|undefined} + * @api + */ +olx.interaction.TranslateOptions.prototype.features; + + /** * @typedef {{condition: (ol.events.ConditionType|undefined), * duration: (number|undefined), diff --git a/src/ol/interaction/translateinteraction.js b/src/ol/interaction/translateinteraction.js new file mode 100644 index 0000000000..09fbe8c349 --- /dev/null +++ b/src/ol/interaction/translateinteraction.js @@ -0,0 +1,180 @@ +goog.provide('ol.interaction.Translate'); + +goog.require('goog.array'); +goog.require('ol.interaction.Pointer'); + + + +/** + * @classdesc + * Interaction for translating (moving) features. + * + * @constructor + * @extends {ol.interaction.Pointer} + * @param {olx.interaction.TranslateOptions} options Options. + * @api + */ +ol.interaction.Translate = function(options) { + goog.base(this, { + handleDownEvent: ol.interaction.Translate.handleDownEvent_, + handleDragEvent: ol.interaction.Translate.handleDragEvent_, + handleMoveEvent: ol.interaction.Translate.handleMoveEvent_, + handleUpEvent: ol.interaction.Translate.handleUpEvent_ + }); + + + /** + * @type {string|undefined} + * @private + */ + this.previousCursor_ = undefined; + + + /** + * The last position we translated to. + * @type {ol.Coordinate} + * @private + */ + this.lastCoordinate_ = null; + + + /** + * @type {ol.Collection.} + * @private + */ + this.features_ = goog.isDef(options.features) ? options.features : null; + + /** + * @type {ol.Feature} + * @private + */ + this.lastFeature_ = null; +}; +goog.inherits(ol.interaction.Translate, ol.interaction.Pointer); + + +/** + * @param {ol.MapBrowserPointerEvent} event Event. + * @return {boolean} Start drag sequence? + * @this {ol.interaction.Translate} + * @private + */ +ol.interaction.Translate.handleDownEvent_ = function(event) { + this.lastFeature_ = this.featuresAtPixel_(event.pixel, event.map); + if (goog.isNull(this.lastCoordinate_) && !goog.isNull(this.lastFeature_)) { + this.lastCoordinate_ = event.coordinate; + ol.interaction.Translate.handleMoveEvent_.call(this, event); + return true; + } + return false; +}; + + +/** + * @param {ol.MapBrowserPointerEvent} event Event. + * @return {boolean} Stop drag sequence? + * @this {ol.interaction.Translate} + * @private + */ +ol.interaction.Translate.handleUpEvent_ = function(event) { + if (!goog.isNull(this.lastCoordinate_)) { + this.lastCoordinate_ = null; + ol.interaction.Translate.handleMoveEvent_.call(this, event); + return true; + } + return false; +}; + + +/** + * @param {ol.MapBrowserPointerEvent} event Event. + * @this {ol.interaction.Translate} + * @private + */ +ol.interaction.Translate.handleDragEvent_ = function(event) { + if (!goog.isNull(this.lastCoordinate_)) { + var newCoordinate = event.coordinate; + var deltaX = newCoordinate[0] - this.lastCoordinate_[0]; + var deltaY = newCoordinate[1] - this.lastCoordinate_[1]; + + if (!goog.isNull(this.features_)) { + this.features_.forEach(function(feature) { + var geom = feature.getGeometry(); + geom.translate(deltaX, deltaY); + feature.setGeometry(geom); + }); + } else if (goog.isNull(this.lastFeature_)) { + var geom = this.lastFeature_.getGeometry(); + geom.translate(deltaX, deltaY); + this.lastFeature_.setGeometry(geom); + } + + this.lastCoordinate_ = newCoordinate; + } +}; + + +/** + * @param {ol.MapBrowserEvent} event Event. + * @this {ol.interaction.Translate} + * @private + */ +ol.interaction.Translate.handleMoveEvent_ = function(event) + { + var elem = event.map.getTargetElement(); + var intersectingFeature = event.map.forEachFeatureAtPixel(event.pixel, + function(feature) { + return feature; + }); + + if (intersectingFeature) { + var isSelected = false; + + if (!goog.isNull(this.features_) && + goog.array.contains(this.features_.getArray(), intersectingFeature)) { + isSelected = true; + } + + this.previousCursor_ = elem.style.cursor; + + // WebKit browsers don't support the grab icons without a prefix + elem.style.cursor = !goog.isNull(this.lastCoordinate_) ? + '-webkit-grabbing' : (isSelected ? '-webkit-grab' : 'pointer'); + + // Thankfully, attempting to set the standard ones will silently fail, + // keeping the prefixed icons + elem.style.cursor = goog.isNull(this.lastCoordinate_) ? + 'grabbing' : (isSelected ? 'grab' : 'pointer'); + + } else { + elem.style.cursor = goog.isDef(this.previousCursor_) ? + this.previousCursor_ : ''; + this.previousCursor_ = undefined; + } +}; + + +/** + * Tests to see if the given coordinates intersects any of our selected + * features. + * @param {ol.Pixel} pixel Pixel coordinate to test for intersection. + * @param {ol.Map} map Map to test the intersection on. + * @return {ol.Feature} Returns the feature found at the specified pixel + * coordinates. + * @private + */ +ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) { + var found = null; + + var intersectingFeature = map.forEachFeatureAtPixel(pixel, + function(feature) { + return feature; + }); + + if (!goog.isNull(this.features_) && + goog.array.contains(this.features_.getArray(), intersectingFeature)) { + found = intersectingFeature; + } + + return found; +}; diff --git a/test/spec/ol/interaction/translateinteraction.test.js b/test/spec/ol/interaction/translateinteraction.test.js new file mode 100644 index 0000000000..8526796ae4 --- /dev/null +++ b/test/spec/ol/interaction/translateinteraction.test.js @@ -0,0 +1,126 @@ +goog.provide('ol.test.interaction.Translate'); + +describe('ol.interaction.Translate', function() { + var target, map, source, features; + + 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); + source = new ol.source.Vector(); + features = [new ol.Feature({ + geometry: new ol.geom.Point([10, -20]) + }), new ol.Feature({ + geometry: new ol.geom.Point([20, -30]) + })]; + source.addFeatures(features); + var layer = new ol.layer.Vector({source: source}); + map = new ol.Map({ + target: target, + layers: [layer], + view: new ol.View({ + projection: 'EPSG:4326', + center: [0, 0], + resolution: 1 + }) + }); + map.on('postrender', function() { + done(); + }); + }); + + afterEach(function() { + goog.dispose(map); + 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. + */ + function simulateEvent(type, x, y, opt_shiftKey) { + var viewport = map.getViewport(); + // calculated in case body has top < 0 (test runner with small window) + var position = goog.style.getClientPosition(viewport); + var shiftKey = goog.isDef(opt_shiftKey) ? opt_shiftKey : false; + var event = new ol.MapBrowserPointerEvent(type, map, + new ol.pointer.PointerEvent(type, + new goog.events.BrowserEvent({ + clientX: position.x + x + width / 2, + clientY: position.y + y + height / 2, + shiftKey: shiftKey + }))); + map.handleMapBrowserEvent(event); + } + + describe('constructor', function() { + + it('creates a new interaction', function() { + var draw = new ol.interaction.Translate({ + features: features + }); + expect(draw).to.be.a(ol.interaction.Translate); + expect(draw).to.be.a(ol.interaction.Interaction); + }); + + }); + + describe('moving features', function() { + var draw; + + beforeEach(function() { + draw = new ol.interaction.Translate({ + features: new ol.Collection([features[0]]) + }); + map.addInteraction(draw); + }); + + it('moves a selected feature', function() { + simulateEvent('pointermove', 10, 20); + simulateEvent('pointerdown', 10, 20); + simulateEvent('pointerdrag', 50, -40); + simulateEvent('pointerup', 50, -40); + var geometry = features[0].getGeometry(); + expect(geometry).to.be.a(ol.geom.Point); + expect(geometry.getCoordinates()).to.eql([50, 40]); + }); + + it('does not move an unselected feature', function() { + simulateEvent('pointermove', 20, 30); + simulateEvent('pointerdown', 20, 30); + simulateEvent('pointerdrag', 50, -40); + simulateEvent('pointerup', 50, -40); + var geometry = features[1].getGeometry(); + expect(geometry).to.be.a(ol.geom.Point); + expect(geometry.getCoordinates()).to.eql([20, -30]); + }); + }); +}); + +goog.require('goog.dispose'); +goog.require('goog.events'); +goog.require('goog.events.BrowserEvent'); +goog.require('goog.style'); +goog.require('ol.Collection'); +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.MapBrowserPointerEvent'); +goog.require('ol.View'); +goog.require('ol.geom.Point'); +goog.require('ol.interaction.Translate'); +goog.require('ol.interaction.Interaction'); +goog.require('ol.layer.Vector'); +goog.require('ol.pointer.PointerEvent'); +goog.require('ol.source.Vector');