diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index 03c5b1f301..4494bca99a 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -2,6 +2,13 @@ ### Next release +#### Changed behavior of the `Draw` interaction + +For better drawing experience, two changes were made to the behavior of the Draw interaction: + + 1. On long press, the current vertex can be dragged to its desired position. + 2. On touch move (e.g. when panning the map on a mobile device), no draw cursor is shown, and the geometry being drawn is not updated. But because of 1., the draw cursor will appear on long press. Mouse moves are not affected by this change. + #### Changes in proj4 integration Because relying on a globally available proj4 is not practical with ES modules, we have made a change to the way we integrate proj4: diff --git a/externs/olx.js b/externs/olx.js index 9ad1f7ed42..ae436811b5 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -2356,6 +2356,7 @@ olx.interaction.DragZoomOptions.prototype.out; * @typedef {{clickTolerance: (number|undefined), * features: (ol.Collection.|undefined), * source: (ol.source.Vector|undefined), + * dragVertexDelay: (number|undefined), * snapTolerance: (number|undefined), * type: (ol.geom.GeometryType|string), * stopClick: (boolean|undefined), @@ -2401,6 +2402,14 @@ olx.interaction.DrawOptions.prototype.features; olx.interaction.DrawOptions.prototype.source; +/** + * Delay in milliseconds after pointerdown before the current vertex can be + * dragged to its exact position. Default is 500 ms. + * @type {number|undefined} + */ +olx.interaction.DrawOptions.prototype.dragVertexDelay; + + /** * Pixel distance for snapping to the drawing finish. Default is `12`. * @type {number|undefined} diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 564dabeff1..5e8dacb92c 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -267,6 +267,8 @@ const PluggableMap = function(options) { */ this.keyHandlerKeys_ = null; + _ol_events_.listen(this.viewport_, EventType.CONTEXTMENU, + this.handleBrowserEvent, this); _ol_events_.listen(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); _ol_events_.listen(this.viewport_, EventType.MOUSEWHEEL, @@ -491,6 +493,8 @@ PluggableMap.prototype.addOverlayInternal_ = function(overlay) { */ PluggableMap.prototype.disposeInternal = function() { this.mapBrowserEventHandler_.dispose(); + _ol_events_.unlisten(this.viewport_, EventType.CONTEXTMENU, + this.handleBrowserEvent, this); _ol_events_.unlisten(this.viewport_, EventType.WHEEL, this.handleBrowserEvent, this); _ol_events_.unlisten(this.viewport_, EventType.MOUSEWHEEL, diff --git a/src/ol/events/EventType.js b/src/ol/events/EventType.js index 78b3b6ae82..3e7689d36b 100644 --- a/src/ol/events/EventType.js +++ b/src/ol/events/EventType.js @@ -15,6 +15,7 @@ export default { CHANGE: 'change', CLEAR: 'clear', + CONTEXTMENU: 'contextmenu', CLICK: 'click', DBLCLICK: 'dblclick', DRAGENTER: 'dragenter', diff --git a/src/ol/interaction/Draw.js b/src/ol/interaction/Draw.js index 1422302fb3..47efd71ec5 100644 --- a/src/ol/interaction/Draw.js +++ b/src/ol/interaction/Draw.js @@ -2,8 +2,10 @@ * @module ol/interaction/Draw */ import {inherits} from '../index.js'; +import EventType from '../events/EventType.js'; import Feature from '../Feature.js'; import MapBrowserEventType from '../MapBrowserEventType.js'; +import MapBrowserPointerEvent from '../MapBrowserPointerEvent.js'; import BaseObject from '../Object.js'; import _ol_coordinate_ from '../coordinate.js'; import _ol_events_ from '../events.js'; @@ -17,6 +19,7 @@ import LineString from '../geom/LineString.js'; import MultiLineString from '../geom/MultiLineString.js'; import MultiPoint from '../geom/MultiPoint.js'; import MultiPolygon from '../geom/MultiPolygon.js'; +import MouseSource from '../pointer/MouseSource.js'; import Point from '../geom/Point.js'; import Polygon, {fromCircle, makeRegular} from '../geom/Polygon.js'; import DrawEventType from '../interaction/DrawEventType.js'; @@ -56,6 +59,18 @@ const Draw = function(options) { */ this.downPx_ = null; + /** + * @type {number} + * @private + */ + this.downTimeout_; + + /** + * @type {number} + * @private + */ + this.lastDragTime_; + /** * @type {boolean} * @private @@ -191,6 +206,12 @@ const Draw = function(options) { */ this.geometryFunction_ = geometryFunction; + /** + * @type {number} + * @private + */ + this.dragVertexDelay_ = options.dragVertexDelay !== undefined ? options.dragVertexDelay : 500; + /** * Finish coordinate for the feature (first point for polygons, last point for * linestrings). @@ -255,7 +276,8 @@ const Draw = function(options) { wrapX: options.wrapX ? options.wrapX : false }), style: options.style ? options.style : - Draw.getDefaultStyleFunction() + Draw.getDefaultStyleFunction(), + updateWhileInteracting: true }); /** @@ -322,8 +344,27 @@ Draw.prototype.setMap = function(map) { * @api */ Draw.handleEvent = function(event) { + if (event.originalEvent.type === EventType.CONTEXTMENU) { + // Avoid context menu for long taps when drawing on mobile + event.preventDefault(); + } this.freehand_ = this.mode_ !== Draw.Mode_.POINT && this.freehandCondition_(event); + let move = event.type === MapBrowserEventType.POINTERMOVE; let pass = true; + if (this.lastDragTime_ && event.type === MapBrowserEventType.POINTERDRAG) { + const now = Date.now(); + if (now - this.lastDragTime_ >= this.dragVertexDelay_) { + this.downPx_ = event.pixel; + this.shouldHandle_ = !this.freehand_; + move = true; + } else { + this.lastDragTime_ = undefined; + } + if (this.shouldHandle_ && this.downTimeout_) { + clearTimeout(this.downTimeout_); + this.downTimeout_ = undefined; + } + } if (this.freehand_ && event.type === MapBrowserEventType.POINTERDRAG && this.sketchFeature_ !== null) { @@ -332,11 +373,18 @@ Draw.handleEvent = function(event) { } else if (this.freehand_ && event.type === MapBrowserEventType.POINTERDOWN) { pass = false; - } else if (event.type === MapBrowserEventType.POINTERMOVE) { - pass = this.handlePointerMove_(event); + } else if (move) { + pass = event.type === MapBrowserEventType.POINTERMOVE; + if (pass && this.freehand_) { + pass = this.handlePointerMove_(event); + } else if (event.pointerEvent.pointerType == MouseSource.POINTER_TYPE || + (event.type === MapBrowserEventType.POINTERDRAG && !this.downTimeout_)) { + this.handlePointerMove_(event); + } } else if (event.type === MapBrowserEventType.DBLCLICK) { pass = false; } + return PointerInteraction.handleEvent.call(this, event) && pass; }; @@ -357,6 +405,11 @@ Draw.handleDownEvent_ = function(event) { } return true; } else if (this.condition_(event)) { + this.lastDragTime_ = Date.now(); + this.downTimeout_ = setTimeout(function() { + this.handlePointerMove_(new MapBrowserPointerEvent( + MapBrowserEventType.POINTERMOVE, event.map, event.pointerEvent, event.frameState)); + }.bind(this), this.dragVertexDelay_); this.downPx_ = event.pixel; return true; } else { @@ -374,6 +427,11 @@ Draw.handleDownEvent_ = function(event) { Draw.handleUpEvent_ = function(event) { let pass = true; + if (this.downTimeout_) { + clearTimeout(this.downTimeout_); + this.downTimeout_ = undefined; + } + this.handlePointerMove_(event); const circleMode = this.mode_ === Draw.Mode_.CIRCLE; @@ -423,6 +481,9 @@ Draw.prototype.handlePointerMove_ = function(event) { this.shouldHandle_ = this.freehand_ ? squaredDistance > this.squaredClickTolerance_ : squaredDistance <= this.squaredClickTolerance_; + if (!this.shouldHandle_) { + return true; + } } if (this.finishCoordinate_) { @@ -505,9 +566,6 @@ Draw.prototype.startDrawing_ = function(event) { this.sketchLineCoords_ = this.sketchCoords_[0]; } else { this.sketchCoords_ = [start.slice(), start.slice()]; - if (this.mode_ === Draw.Mode_.CIRCLE) { - this.sketchLineCoords_ = this.sketchCoords_; - } } if (this.sketchLineCoords_) { this.sketchLine_ = new Feature( diff --git a/test/spec/ol/interaction/draw.test.js b/test/spec/ol/interaction/draw.test.js index 1fc1b912bd..693fe913bf 100644 --- a/test/spec/ol/interaction/draw.test.js +++ b/test/spec/ol/interaction/draw.test.js @@ -103,6 +103,15 @@ describe('ol.interaction.Draw', function() { expect(draw.freehandCondition_(event)).to.be(true); }); + it('accepts a dragVertexDelay option', function() { + const draw = new Draw({ + source: source, + type: 'LineString', + dragVertexDelay: 42 + }); + expect(draw.dragVertexDelay_).to.be(42); + }); + }); describe('specifying a geometryName', function() { @@ -339,7 +348,6 @@ describe('ol.interaction.Draw', function() { simulateEvent('pointermove', 20, 30); // freehand - simulateEvent('pointerdown', 20, 30, true); simulateEvent('pointermove', 20, 30, true); simulateEvent('pointerdrag', 20, 30, true); simulateEvent('pointermove', 30, 40, true); @@ -396,6 +404,38 @@ describe('ol.interaction.Draw', function() { expect(geometry.getCoordinates()).to.eql([[10, -20], [30, -20]]); }); + it('allows dragging of the vertex after dragVertexDelay', function(done) { + // first point + simulateEvent('pointermove', 10, 20); + simulateEvent('pointerdown', 10, 20); + simulateEvent('pointerup', 10, 20); + + // second point, drag vertex + simulateEvent('pointermove', 15, 20); + simulateEvent('pointerdown', 15, 20); + setTimeout(function() { + simulateEvent('pointermove', 20, 10); + simulateEvent('pointerdrag', 20, 10); + simulateEvent('pointerup', 20, 10); + // third point + simulateEvent('pointermove', 30, 20); + simulateEvent('pointerdown', 30, 20); + simulateEvent('pointerup', 30, 20); + + // finish on third point + simulateEvent('pointerdown', 30, 20); + simulateEvent('pointerup', 30, 20); + + const features = source.getFeatures(); + expect(features).to.have.length(1); + const geometry = features[0].getGeometry(); + expect(geometry).to.be.a(LineString); + expect(geometry.getCoordinates()).to.eql([[10, -20], [20, -10], [30, -20]]); + + done(); + }, 600); + }); + it('triggers draw events', function() { const ds = sinon.spy(); const de = sinon.spy();