From b3bcfb5b417909cae1b3a1f87db000e9449b0139 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 24 Nov 2020 20:22:24 +0100 Subject: [PATCH 01/15] Modify with hit detection support --- src/ol/interaction/Modify.js | 112 ++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 23 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 3c946bffc4..eafa0d3d0a 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -111,11 +111,14 @@ const ModifyEventType = { * Style used for the features being modified. By default the default edit * style is used (see {@link module:ol/style}). * @property {VectorSource} [source] The vector source with - * features to modify. If a vector source is not provided, a feature collection - * must be provided with the features option. + * features to modify. If a vector source is not provided, a layer or feature collection + * must be provided with the `layer` or `features` option. + * @property {import("../layer/BaseVector").default} [layer] The layer with + * features to modify. If a layer is not provided, a vector source or feature collection + * must be provided with the `source` or `features` option. * @property {Collection} [features] * The features the interaction works on. If a feature collection is not - * provided, a vector source must be provided with the source option. + * provided, a layer or vector source must be provided with the `layer` or `source` option. * @property {boolean} [wrapX=false] Wrap the world horizontally on the sketch * overlay. */ @@ -159,7 +162,15 @@ export class ModifyEvent extends Event { * `source` option. If you want to modify features in a collection (for example, * the collection used by a select interaction), construct the interaction with * the `features` option. The interaction must be constructed with either a - * `source` or `features` option. + * `source`, `features` or `layer` option. + * + * When configured with a `source` or `features`, the modification object (for + * point geometries the point, for linestring or polygon geometries an existing + * vertex or a new vertex along a segment) is determined by geometric proximity to + * the pointer location. When configured with a `layer`, hit detection will be + * used to determine the feature that will be modified. This is the preferred way + * when the visual representation of the features subject to modification is much + * different from their geometry (e.g. icons with an offset). * * By default, the interaction will allow deletion of vertices when the `alt` * key is pressed. To configure the interaction with a different condition @@ -314,6 +325,11 @@ class Modify extends PointerInteraction { */ this.source_ = null; + /** + * @type {import("../layer/BaseVector").default} + */ + this.layer_ = null; + let features; if (options.source) { this.source_ = options.source; @@ -326,15 +342,20 @@ class Modify extends PointerInteraction { VectorEventType.REMOVEFEATURE, this.handleSourceRemove_.bind(this) ); - } else { + } else if (options.features) { features = options.features; + } else if (options.layer) { + features = new Collection(); + this.layer_ = options.layer; } if (!features) { - throw new Error('The modify interaction requires features or a source'); + throw new Error( + 'The modify interaction requires features, a source or a layer' + ); } /** - * @type {Collection} + * @type {Collection} * @private */ this.features_ = features; @@ -354,6 +375,12 @@ class Modify extends PointerInteraction { * @private */ this.lastPointerEvent_ = null; + + /** + * Delta (x, y in map units) between matched rtree vertex and pointer vertex. + * @type {Array} + */ + this.delta_ = [0, 0]; } /** @@ -786,7 +813,10 @@ class Modify extends PointerInteraction { this.ignoreNextSingleClick_ = false; this.willModifyFeatures_(evt); - const vertex = evt.coordinate; + const vertex = [ + evt.coordinate[0] + this.delta_[0], + evt.coordinate[1] + this.delta_[1], + ]; for (let i = 0, ii = this.dragSegments_.length; i < ii; ++i) { const dragSegment = this.dragSegments_[i]; const segmentData = dragSegment[0]; @@ -1048,32 +1078,68 @@ class Modify extends PointerInteraction { ); }; - const viewExtent = fromUserExtent( - createExtent(pixelCoordinate, tempExtent), - projection - ); - const buffer = map.getView().getResolution() * this.pixelTolerance_; - const box = toUserExtent( - bufferExtent(viewExtent, buffer, tempExtent), - projection - ); - const rBush = this.rBush_; - const nodes = rBush.getInExtent(box); - if (nodes.length > 0) { + /** @type {import("../geom/SimpleGeometry").default} */ + let geometry; + let point = false; + let nodes; + if (this.layer_) { + const feature = map.forEachFeatureAtPixel( + pixel, + (feature, layer, geom) => { + geometry = geom || feature.getGeometry(); + return feature; + }, + { + layerFilter: (layer) => layer === this.layer_, + hitTolerance: this.pixelTolerance_, + } + ); + if (feature && feature !== this.features_.item(0)) { + this.features_.setAt(0, feature); + } + if (!feature) { + this.features_.clear(); + } + if (geometry) { + nodes = rBush.getInExtent(geometry.getExtent()); + const type = geometry.getType(); + if (type === GeometryType.POINT || type === GeometryType.MULTI_POINT) { + point = true; + } + } + } else { + const viewExtent = fromUserExtent( + createExtent(pixelCoordinate, tempExtent), + projection + ); + const buffer = map.getView().getResolution() * this.pixelTolerance_; + const box = toUserExtent( + bufferExtent(viewExtent, buffer, tempExtent), + projection + ); + nodes = rBush.getInExtent(box); + } + + if (nodes && nodes.length > 0) { nodes.sort(sortByDistance); const node = nodes[0]; + if (!geometry) { + geometry = node.geometry; + } const closestSegment = node.segment; let vertex = closestOnSegmentData(pixelCoordinate, node, projection); const vertexPixel = map.getPixelFromCoordinate(vertex); let dist = coordinateDistance(pixel, vertexPixel); - if (dist <= this.pixelTolerance_) { + if (point || dist <= this.pixelTolerance_) { /** @type {Object} */ const vertexSegments = {}; vertexSegments[getUid(closestSegment)] = true; + this.delta_[0] = point ? vertex[0] - pixelCoordinate[0] : 0; + this.delta_[1] = point ? vertex[1] - pixelCoordinate[1] : 0; if ( - node.geometry.getType() === GeometryType.CIRCLE && + geometry.getType() === GeometryType.CIRCLE && node.index === CIRCLE_CIRCUMFERENCE_INDEX ) { this.snappedToVertex_ = true; @@ -1093,7 +1159,7 @@ class Modify extends PointerInteraction { } this.createOrUpdateVertexFeature_(vertex); const geometries = {}; - geometries[getUid(node.geometry)] = true; + geometries[getUid(geometry)] = true; for (let i = 1, ii = nodes.length; i < ii; ++i) { const segment = nodes[i].segment; if ( From 6874bfaaef59e10e8ef61875de61647258a4765b Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 24 Nov 2020 22:43:16 +0100 Subject: [PATCH 02/15] Report features actually being modified, not all --- src/ol/interaction/Modify.js | 53 +++++++++++++++++-------- test/spec/ol/interaction/modify.test.js | 1 + 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index eafa0d3d0a..39956c6d04 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -248,10 +248,10 @@ class Modify extends PointerInteraction { this.ignoreNextSingleClick_ = false; /** - * @type {boolean} + * @type {Array} * @private */ - this.modified_ = false; + this.featuresBeingModified_ = null; /** * Segment RTree for each layer @@ -403,14 +403,27 @@ class Modify extends PointerInteraction { } /** - * @param {import("../MapBrowserEvent.js").default} evt Map browser event + * @param {import("../MapBrowserEvent.js").default} evt Map browser event. + * @param {Array>} segments The segments subject to modification. * @private */ - willModifyFeatures_(evt) { - if (!this.modified_) { - this.modified_ = true; + willModifyFeatures_(evt, segments) { + if (!this.featuresBeingModified_) { + this.featuresBeingModified_ = []; + const features = this.featuresBeingModified_; + for (let i = 0, ii = segments.length; i < ii; ++i) { + const feature = segments[i][0].feature; + if (features.indexOf(feature) === -1) { + features.push(feature); + } + } + this.dispatchEvent( - new ModifyEvent(ModifyEventType.MODIFYSTART, this.features_, evt) + new ModifyEvent( + ModifyEventType.MODIFYSTART, + new Collection(features), + evt + ) ); } } @@ -811,7 +824,7 @@ class Modify extends PointerInteraction { */ handleDragEvent(evt) { this.ignoreNextSingleClick_ = false; - this.willModifyFeatures_(evt); + this.willModifyFeatures_(evt, this.dragSegments_); const vertex = [ evt.coordinate[0] + this.delta_[0], @@ -914,7 +927,7 @@ class Modify extends PointerInteraction { const pixelCoordinate = evt.coordinate; this.handlePointerAtPixel_(evt.pixel, evt.map, pixelCoordinate); this.dragSegments_.length = 0; - this.modified_ = false; + this.featuresBeingModified_ = null; const vertexFeature = this.vertexFeature_; if (vertexFeature) { const projection = evt.map.getView().getProjection(); @@ -995,7 +1008,7 @@ class Modify extends PointerInteraction { } if (insertVertices.length) { - this.willModifyFeatures_(evt); + this.willModifyFeatures_(evt, [insertVertices]); } for (let j = insertVertices.length - 1; j >= 0; --j) { @@ -1044,11 +1057,15 @@ class Modify extends PointerInteraction { this.rBush_.update(boundingExtent(segmentData.segment), segmentData); } } - if (this.modified_) { + if (this.featuresBeingModified_) { this.dispatchEvent( - new ModifyEvent(ModifyEventType.MODIFYEND, this.features_, evt) + new ModifyEvent( + ModifyEventType.MODIFYEND, + new Collection(this.featuresBeingModified_), + evt + ) ); - this.modified_ = false; + this.featuresBeingModified_ = null; } return false; } @@ -1269,12 +1286,16 @@ class Modify extends PointerInteraction { this.lastPointerEvent_.type != MapBrowserEventType.POINTERDRAG ) { const evt = this.lastPointerEvent_; - this.willModifyFeatures_(evt); + this.willModifyFeatures_(evt, this.dragSegments_); const removed = this.removeVertex_(); this.dispatchEvent( - new ModifyEvent(ModifyEventType.MODIFYEND, this.features_, evt) + new ModifyEvent( + ModifyEventType.MODIFYEND, + new Collection(this.featuresBeingModified_), + evt + ) ); - this.modified_ = false; + this.featuresBeingModified_ = null; return removed; } return false; diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 367033ce8c..925960ec54 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -562,6 +562,7 @@ describe('ol.interaction.Modify', function () { let modify, feature, events; beforeEach(function () { + features.push(new Feature(new Point([12, 34]))); modify = new Modify({ features: new Collection(features), }); From ec9dde88f9a28ed418fe7fb29a4433dc42cdd6b1 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 24 Nov 2020 22:44:13 +0100 Subject: [PATCH 03/15] Add features property to vertex feature --- src/ol/interaction/Modify.js | 22 ++++++++++++++++------ test/spec/ol/interaction/modify.test.js | 5 +++-- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 39956c6d04..24dbde582b 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -108,8 +108,11 @@ const ModifyEventType = { * @property {number} [pixelTolerance=10] Pixel tolerance for considering the * pointer close enough to a segment or vertex for editing. * @property {import("../style/Style.js").StyleLike} [style] - * Style used for the features being modified. By default the default edit - * style is used (see {@link module:ol/style}). + * Style used for the modification point. For linestrings and polygons, this will + * be the affected vertex, for circles a point along the circle, and for points the actual + * point. If not configured, the default edit style is used (see {@link module:ol/style}). + * When using a style function, the point feature passed to the function will have a `features` + * property - an array whose entries are the features that are being modified. * @property {VectorSource} [source] The vector source with * features to modify. If a vector source is not provided, a layer or feature collection * must be provided with the `layer` or `features` option. @@ -765,10 +768,11 @@ class Modify extends PointerInteraction { /** * @param {import("../coordinate.js").Coordinate} coordinates Coordinates. + * @param {Array} features The features being modified. * @return {Feature} Vertex feature. * @private */ - createOrUpdateVertexFeature_(coordinates) { + createOrUpdateVertexFeature_(coordinates, features) { let vertexFeature = this.vertexFeature_; if (!vertexFeature) { vertexFeature = new Feature(new Point(coordinates)); @@ -778,6 +782,7 @@ class Modify extends PointerInteraction { const geometry = vertexFeature.getGeometry(); geometry.setCoordinates(coordinates); } + vertexFeature.set('features', features); return vertexFeature; } @@ -830,9 +835,14 @@ class Modify extends PointerInteraction { evt.coordinate[0] + this.delta_[0], evt.coordinate[1] + this.delta_[1], ]; + const features = []; for (let i = 0, ii = this.dragSegments_.length; i < ii; ++i) { const dragSegment = this.dragSegments_[i]; const segmentData = dragSegment[0]; + const feature = segmentData.feature; + if (features.indexOf(feature) === -1) { + features.push(feature); + } const depth = segmentData.depth; const geometry = segmentData.geometry; let coordinates; @@ -912,7 +922,7 @@ class Modify extends PointerInteraction { this.setGeometryCoordinates_(geometry, coordinates); } } - this.createOrUpdateVertexFeature_(vertex); + this.createOrUpdateVertexFeature_(vertex, features); } /** @@ -1160,7 +1170,7 @@ class Modify extends PointerInteraction { node.index === CIRCLE_CIRCUMFERENCE_INDEX ) { this.snappedToVertex_ = true; - this.createOrUpdateVertexFeature_(vertex); + this.createOrUpdateVertexFeature_(vertex, [node.feature]); } else { const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); @@ -1174,7 +1184,7 @@ class Modify extends PointerInteraction { ? closestSegment[1] : closestSegment[0]; } - this.createOrUpdateVertexFeature_(vertex); + this.createOrUpdateVertexFeature_(vertex, [node.feature]); const geometries = {}; geometries[getUid(geometry)] = true; for (let i = 1, ii = nodes.length; i < ii; ++i) { diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 925960ec54..0bd406eb74 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -942,8 +942,8 @@ describe('ol.interaction.Modify', function () { }); }); - describe('#setActive', function () { - it('removes the vertexFeature of deactivation', function () { + describe('Vertex feature', function () { + it('tracks features and removes the vertexFeature on deactivation', function () { const modify = new Modify({ features: new Collection(features), }); @@ -952,6 +952,7 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointermove', 10, -20, null, 0); expect(modify.vertexFeature_).to.not.be(null); + expect(modify.vertexFeature_.get('features').length).to.be(1); modify.setActive(false); expect(modify.vertexFeature_).to.be(null); From 314724d8807e13a88daba8604c987a45d4148553 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 27 Nov 2020 20:03:51 +0100 Subject: [PATCH 04/15] Add geometries property to vertex feature --- src/ol/interaction/Modify.js | 29 +++++++++++---- test/spec/ol/interaction/modify.test.js | 48 +++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 24dbde582b..d27ed3a154 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -112,7 +112,10 @@ const ModifyEventType = { * be the affected vertex, for circles a point along the circle, and for points the actual * point. If not configured, the default edit style is used (see {@link module:ol/style}). * When using a style function, the point feature passed to the function will have a `features` - * property - an array whose entries are the features that are being modified. + * property - an array whose entries are the features that are being modified, and a `geometries` + * property - an array whose entries are the geometries that are being modified. Both arrays are + * in the same order. The `geometries` are only useful when modifying geometry collections, where + * the geometry will be the particular geometry from the collection that is being modified. * @property {VectorSource} [source] The vector source with * features to modify. If a vector source is not provided, a layer or feature collection * must be provided with the `layer` or `features` option. @@ -769,10 +772,11 @@ class Modify extends PointerInteraction { /** * @param {import("../coordinate.js").Coordinate} coordinates Coordinates. * @param {Array} features The features being modified. + * @param {Array} geometries The geometries being modified. * @return {Feature} Vertex feature. * @private */ - createOrUpdateVertexFeature_(coordinates, features) { + createOrUpdateVertexFeature_(coordinates, features, geometries) { let vertexFeature = this.vertexFeature_; if (!vertexFeature) { vertexFeature = new Feature(new Point(coordinates)); @@ -783,6 +787,7 @@ class Modify extends PointerInteraction { geometry.setCoordinates(coordinates); } vertexFeature.set('features', features); + vertexFeature.set('geometries', geometries); return vertexFeature; } @@ -836,6 +841,7 @@ class Modify extends PointerInteraction { evt.coordinate[1] + this.delta_[1], ]; const features = []; + const geometries = []; for (let i = 0, ii = this.dragSegments_.length; i < ii; ++i) { const dragSegment = this.dragSegments_[i]; const segmentData = dragSegment[0]; @@ -843,8 +849,11 @@ class Modify extends PointerInteraction { if (features.indexOf(feature) === -1) { features.push(feature); } - const depth = segmentData.depth; const geometry = segmentData.geometry; + if (geometries.indexOf(geometry) === -1) { + geometries.push(geometry); + } + const depth = segmentData.depth; let coordinates; const segment = segmentData.segment; const index = dragSegment[1]; @@ -922,7 +931,7 @@ class Modify extends PointerInteraction { this.setGeometryCoordinates_(geometry, coordinates); } } - this.createOrUpdateVertexFeature_(vertex, features); + this.createOrUpdateVertexFeature_(vertex, features, geometries); } /** @@ -1170,7 +1179,11 @@ class Modify extends PointerInteraction { node.index === CIRCLE_CIRCUMFERENCE_INDEX ) { this.snappedToVertex_ = true; - this.createOrUpdateVertexFeature_(vertex, [node.feature]); + this.createOrUpdateVertexFeature_( + vertex, + [node.feature], + [node.geometry] + ); } else { const pixel1 = map.getPixelFromCoordinate(closestSegment[0]); const pixel2 = map.getPixelFromCoordinate(closestSegment[1]); @@ -1184,7 +1197,11 @@ class Modify extends PointerInteraction { ? closestSegment[1] : closestSegment[0]; } - this.createOrUpdateVertexFeature_(vertex, [node.feature]); + this.createOrUpdateVertexFeature_( + vertex, + [node.feature], + [node.geometry] + ); const geometries = {}; geometries[getUid(geometry)] = true; for (let i = 1, ii = nodes.length; i < ii; ++i) { diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 0bd406eb74..3b9b383d35 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -8,11 +8,12 @@ import Map from '../../../../src/ol/Map.js'; import MapBrowserEvent from '../../../../src/ol/MapBrowserEvent.js'; import Modify, {ModifyEvent} from '../../../../src/ol/interaction/Modify.js'; import Point from '../../../../src/ol/geom/Point.js'; -import Polygon from '../../../../src/ol/geom/Polygon.js'; +import Polygon, {fromExtent} from '../../../../src/ol/geom/Polygon.js'; import Snap from '../../../../src/ol/interaction/Snap.js'; import VectorLayer from '../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; import View from '../../../../src/ol/View.js'; +import {MultiPoint} from '../../../../src/ol/geom.js'; import { clearUserProjection, setUserProjection, @@ -943,9 +944,10 @@ describe('ol.interaction.Modify', function () { }); describe('Vertex feature', function () { - it('tracks features and removes the vertexFeature on deactivation', function () { + it('tracks features and geometries and removes the vertexFeature on deactivation', function () { + const collection = new Collection(features); const modify = new Modify({ - features: new Collection(features), + features: collection, }); map.addInteraction(modify); expect(modify.vertexFeature_).to.be(null); @@ -953,9 +955,49 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointermove', 10, -20, null, 0); expect(modify.vertexFeature_).to.not.be(null); expect(modify.vertexFeature_.get('features').length).to.be(1); + expect(modify.vertexFeature_.get('geometries').length).to.be(1); modify.setActive(false); expect(modify.vertexFeature_).to.be(null); + map.removeInteraction(modify); + }); + + it('tracks features and geometries - multi geometry', function () { + const collection = new Collection(); + const modify = new Modify({ + features: collection, + }); + map.addInteraction(modify); + const feature = new Feature( + new MultiPoint([ + [10, 10], + [10, 20], + ]) + ); + collection.push(feature); + simulateEvent('pointermove', 10, -20, null, 0); + expect(modify.vertexFeature_.get('features')[0]).to.eql(feature); + expect(modify.vertexFeature_.get('geometries')[0]).to.eql( + feature.getGeometry() + ); + map.removeInteraction(modify); + }); + + it('tracks features and geometries - geometry collection', function () { + const collection = new Collection(); + const modify = new Modify({ + features: collection, + }); + map.addInteraction(modify); + const feature = new Feature( + new GeometryCollection([fromExtent([0, 0, 10, 10]), new Point([5, 5])]) + ); + collection.push(feature); + simulateEvent('pointermove', 5, -5, null, 0); + expect(modify.vertexFeature_.get('features')[0]).to.eql(feature); + expect(modify.vertexFeature_.get('geometries')[0]).to.eql( + feature.getGeometry().getGeometriesArray()[1] + ); }); }); From db6eb040d233400800b968cfcc5d428e8a853411 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 27 Nov 2020 23:45:32 +0100 Subject: [PATCH 05/15] Improve documentation --- src/ol/interaction/Modify.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index d27ed3a154..2be086ddad 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -170,13 +170,15 @@ export class ModifyEvent extends Event { * the `features` option. The interaction must be constructed with either a * `source`, `features` or `layer` option. * - * When configured with a `source` or `features`, the modification object (for - * point geometries the point, for linestring or polygon geometries an existing - * vertex or a new vertex along a segment) is determined by geometric proximity to - * the pointer location. When configured with a `layer`, hit detection will be - * used to determine the feature that will be modified. This is the preferred way - * when the visual representation of the features subject to modification is much - * different from their geometry (e.g. icons with an offset). + * When configured with a `source` or `features`, Cartesian distance from the + * pointer is used to determine all features that will be modified. This is the + * preferred mode for modifying polygons or linestrings with shared edges or + * vertices that have to be modified together to maintain topology. + * + * When configured with a `layer`, pointer hit detection is used to determine the + * topmost feature that will be modified. This is the preferred mode for modifying + * points when the visual representation is much different from + * their geometry (e.g. large icons or icons with an offset). * * By default, the interaction will allow deletion of vertices when the `alt` * key is pressed. To configure the interaction with a different condition From 9b31deb38f9f4408c82ee9da1bf430d6a86fb78e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 28 Nov 2020 11:03:56 +0100 Subject: [PATCH 06/15] Simplify API and code --- src/ol/interaction/Modify.js | 111 +++++++++++------------- test/spec/ol/interaction/modify.test.js | 49 +++++++++++ 2 files changed, 99 insertions(+), 61 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 2be086ddad..487bb142af 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -120,8 +120,10 @@ const ModifyEventType = { * features to modify. If a vector source is not provided, a layer or feature collection * must be provided with the `layer` or `features` option. * @property {import("../layer/BaseVector").default} [layer] The layer with - * features to modify. If a layer is not provided, a vector source or feature collection - * must be provided with the `source` or `features` option. + * features to modify. When provided, point features will considered for modification based on + * their visual appearance on this layer, instead of being within the `pixelTolerance` from the + * pointer location. When no `source` or `features` are configured, this layer's source will + * be used as source for modification candidates. * @property {Collection} [features] * The features the interaction works on. If a feature collection is not * provided, a layer or vector source must be provided with the `layer` or `source` option. @@ -170,15 +172,12 @@ export class ModifyEvent extends Event { * the `features` option. The interaction must be constructed with either a * `source`, `features` or `layer` option. * - * When configured with a `source` or `features`, Cartesian distance from the - * pointer is used to determine all features that will be modified. This is the - * preferred mode for modifying polygons or linestrings with shared edges or - * vertices that have to be modified together to maintain topology. - * - * When configured with a `layer`, pointer hit detection is used to determine the - * topmost feature that will be modified. This is the preferred mode for modifying - * points when the visual representation is much different from - * their geometry (e.g. large icons or icons with an offset). + * Cartesian distance from the pointer is used to determine the features that + * will be modified. This means that geometries will only be considered for + * modification when they are within the configured `pixelTolerane`. For point + * geometries, hit detection can be used to match their visual appearance. To + * enable hit detection, the interaction has to be configured with the `layer` + * that contains the points. * * By default, the interaction will allow deletion of vertices when the `alt` * key is pressed. To configure the interaction with a different condition @@ -339,21 +338,28 @@ class Modify extends PointerInteraction { this.layer_ = null; let features; - if (options.source) { - this.source_ = options.source; - features = new Collection(this.source_.getFeatures()); - this.source_.addEventListener( - VectorEventType.ADDFEATURE, - this.handleSourceAdd_.bind(this) - ); - this.source_.addEventListener( - VectorEventType.REMOVEFEATURE, - this.handleSourceRemove_.bind(this) - ); - } else if (options.features) { + if (options.features) { features = options.features; - } else if (options.layer) { - features = new Collection(); + } else { + const source = options.source + ? options.source + : options.layer + ? options.layer.getSource() + : undefined; + if (source) { + this.source_ = source; + features = new Collection(this.source_.getFeatures()); + this.source_.addEventListener( + VectorEventType.ADDFEATURE, + this.handleSourceAdd_.bind(this) + ); + this.source_.addEventListener( + VectorEventType.REMOVEFEATURE, + this.handleSourceRemove_.bind(this) + ); + } + } + if (options.layer) { this.layer_ = options.layer; } if (!features) { @@ -1116,68 +1122,51 @@ class Modify extends PointerInteraction { ); }; - const rBush = this.rBush_; - /** @type {import("../geom/SimpleGeometry").default} */ - let geometry; - let point = false; - let nodes; + let box, hitPointGeometry; if (this.layer_) { - const feature = map.forEachFeatureAtPixel( + map.forEachFeatureAtPixel( pixel, - (feature, layer, geom) => { - geometry = geom || feature.getGeometry(); - return feature; + (feature, layer, geometry) => { + geometry = geometry || feature.getGeometry(); + if (geometry.getType() === GeometryType.POINT) { + hitPointGeometry = geometry; + box = hitPointGeometry.getExtent(); + } + return true; }, { layerFilter: (layer) => layer === this.layer_, - hitTolerance: this.pixelTolerance_, } ); - if (feature && feature !== this.features_.item(0)) { - this.features_.setAt(0, feature); - } - if (!feature) { - this.features_.clear(); - } - if (geometry) { - nodes = rBush.getInExtent(geometry.getExtent()); - const type = geometry.getType(); - if (type === GeometryType.POINT || type === GeometryType.MULTI_POINT) { - point = true; - } - } - } else { + } + if (!box) { const viewExtent = fromUserExtent( createExtent(pixelCoordinate, tempExtent), projection ); const buffer = map.getView().getResolution() * this.pixelTolerance_; - const box = toUserExtent( + box = toUserExtent( bufferExtent(viewExtent, buffer, tempExtent), projection ); - nodes = rBush.getInExtent(box); } + const nodes = this.rBush_.getInExtent(box); if (nodes && nodes.length > 0) { - nodes.sort(sortByDistance); - const node = nodes[0]; - if (!geometry) { - geometry = node.geometry; - } + const node = nodes.sort(sortByDistance)[0]; const closestSegment = node.segment; let vertex = closestOnSegmentData(pixelCoordinate, node, projection); const vertexPixel = map.getPixelFromCoordinate(vertex); let dist = coordinateDistance(pixel, vertexPixel); - if (point || dist <= this.pixelTolerance_) { + if (hitPointGeometry || dist <= this.pixelTolerance_) { /** @type {Object} */ const vertexSegments = {}; vertexSegments[getUid(closestSegment)] = true; - this.delta_[0] = point ? vertex[0] - pixelCoordinate[0] : 0; - this.delta_[1] = point ? vertex[1] - pixelCoordinate[1] : 0; + this.delta_[0] = hitPointGeometry ? vertex[0] - pixelCoordinate[0] : 0; + this.delta_[1] = hitPointGeometry ? vertex[1] - pixelCoordinate[1] : 0; if ( - geometry.getType() === GeometryType.CIRCLE && + node.geometry.getType() === GeometryType.CIRCLE && node.index === CIRCLE_CIRCUMFERENCE_INDEX ) { this.snappedToVertex_ = true; @@ -1205,7 +1194,7 @@ class Modify extends PointerInteraction { [node.geometry] ); const geometries = {}; - geometries[getUid(geometry)] = true; + geometries[getUid(node.geometry)] = true; for (let i = 1, ii = nodes.length; i < ii; ++i) { const segment = nodes[i].segment; if ( diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 3b9b383d35..108a18d86c 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -195,6 +195,55 @@ describe('ol.interaction.Modify', function () { expect(rbushEntries.length).to.be(1); expect(rbushEntries[0].feature).to.be(feature); }); + + it('accepts a layer for modification features', function () { + const feature = new Feature(new Point([0, 0])); + const source = new VectorSource({features: [feature]}); + const layer = new VectorLayer({source: source}); + const modify = new Modify({layer: layer}); + const rbushEntries = modify.rBush_.getAll(); + expect(rbushEntries.length).to.be(1); + expect(rbushEntries[0].feature).to.be(feature); + expect(modify.layer_).to.be(layer); + }); + + it('accepts a layer in addition to a features collection', function () { + const feature = new Feature(new Point([0, 0])); + const source = new VectorSource({features: [feature]}); + const layer = new VectorLayer({source: source}); + const features = new Collection([new Feature(new Point([1, 1]))]); + const modify = new Modify({layer: layer, features: features}); + const rbushEntries = modify.rBush_.getAll(); + expect(rbushEntries.length).to.be(1); + expect(rbushEntries[0].feature).to.be(features.item(0)); + expect(modify.layer_).to.be(layer); + }); + + it('accepts a layer in addition to a features collection', function () { + const feature = new Feature(new Point([0, 0])); + const source = new VectorSource({features: [feature]}); + const layer = new VectorLayer({source: source}); + const features = new Collection([new Feature(new Point([1, 1]))]); + const modify = new Modify({layer: layer, features: features}); + const rbushEntries = modify.rBush_.getAll(); + expect(rbushEntries.length).to.be(1); + expect(rbushEntries[0].feature).to.be(features.item(0)); + expect(modify.layer_).to.be(layer); + }); + + it('accepts a layer in addition to a source', function () { + const feature = new Feature(new Point([0, 0])); + const source = new VectorSource({features: [feature]}); + const layer = new VectorLayer({source: source}); + const candidateSource = new VectorSource({ + features: [new Feature(new Point([1, 1]))], + }); + const modify = new Modify({layer: layer, source: candidateSource}); + const rbushEntries = modify.rBush_.getAll(); + expect(rbushEntries.length).to.be(1); + expect(rbushEntries[0].feature).to.be(candidateSource.getFeatures()[0]); + expect(modify.layer_).to.be(layer); + }); }); describe('vertex deletion', function () { From d0a1c10cec48c8260c8acf3e422eb434f4b80df0 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 28 Nov 2020 13:09:36 +0100 Subject: [PATCH 07/15] Add example for Modify with hit detection --- examples/modify-icon.html | 10 ++++++ examples/modify-icon.js | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 examples/modify-icon.html create mode 100644 examples/modify-icon.js diff --git a/examples/modify-icon.html b/examples/modify-icon.html new file mode 100644 index 0000000000..37d936c61c --- /dev/null +++ b/examples/modify-icon.html @@ -0,0 +1,10 @@ +--- +layout: example.html +title: Icon modification +shortdesc: Example using a Modify interaction to edit an icon. +docs: > + The icon on this map can be dragged to modify its location. +

The Modify interaction can be configured with a `layer` option. With this option, hit detection will be used to determine the modification candidate.

+tags: "vector, modify, icon, marker" +--- +
diff --git a/examples/modify-icon.js b/examples/modify-icon.js new file mode 100644 index 0000000000..13f470ab7c --- /dev/null +++ b/examples/modify-icon.js @@ -0,0 +1,67 @@ +import Feature from '../src/ol/Feature.js'; +import Map from '../src/ol/Map.js'; +import Point from '../src/ol/geom/Point.js'; +import TileJSON from '../src/ol/source/TileJSON.js'; +import VectorSource from '../src/ol/source/Vector.js'; +import View from '../src/ol/View.js'; +import {Icon, Style} from '../src/ol/style.js'; +import {Modify} from '../src/ol/interaction.js'; +import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js'; + +const iconFeature = new Feature({ + geometry: new Point([0, 0]), + name: 'Null Island', + population: 4000, + rainfall: 500, +}); + +const iconStyle = new Style({ + image: new Icon({ + anchor: [0.5, 46], + anchorXUnits: 'fraction', + anchorYUnits: 'pixels', + src: 'data/icon.png', + }), +}); + +iconFeature.setStyle(iconStyle); + +const vectorSource = new VectorSource({ + features: [iconFeature], +}); + +const vectorLayer = new VectorLayer({ + source: vectorSource, +}); + +const rasterLayer = new TileLayer({ + source: new TileJSON({ + url: 'https://a.tiles.mapbox.com/v3/aj.1x1-degrees.json?secure=1', + crossOrigin: '', + }), +}); + +const target = document.getElementById('map'); +const map = new Map({ + layers: [rasterLayer, vectorLayer], + target: target, + view: new View({ + center: [0, 0], + zoom: 3, + }), +}); + +const modify = new Modify({ + layer: vectorLayer, + style: function () {}, // do not render the modification vertex +}); +modify.on(['modifystart', 'modifyend'], function (evt) { + target.style.cursor = evt.type === 'modifystart' ? 'grabbing' : 'pointer'; +}); +map.on('pointermove', function (evt) { + if (!evt.dragging) { + target.style.cursor = map.hasFeatureAtPixel(evt.pixel) ? 'pointer' : ''; + } +}); + +map.addInteraction(modify); From 128d20abf3ca9e9befbb9acf82070f588170d820 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 1 Dec 2020 13:47:40 +0100 Subject: [PATCH 08/15] Simpler API with hitDetection option --- examples/modify-icon.js | 4 +- src/ol/interaction/Modify.js | 71 ++++++++++------------- test/spec/ol/interaction/modify.test.js | 77 +++++++++++-------------- 3 files changed, 68 insertions(+), 84 deletions(-) diff --git a/examples/modify-icon.js b/examples/modify-icon.js index 13f470ab7c..0efee5e415 100644 --- a/examples/modify-icon.js +++ b/examples/modify-icon.js @@ -52,8 +52,8 @@ const map = new Map({ }); const modify = new Modify({ - layer: vectorLayer, - style: function () {}, // do not render the modification vertex + hitDetection: vectorLayer, + source: vectorSource, }); modify.on(['modifystart', 'modifyend'], function (evt) { target.style.cursor = evt.type === 'modifystart' ? 'grabbing' : 'pointer'; diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 487bb142af..1176dc95f5 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -117,16 +117,15 @@ const ModifyEventType = { * in the same order. The `geometries` are only useful when modifying geometry collections, where * the geometry will be the particular geometry from the collection that is being modified. * @property {VectorSource} [source] The vector source with - * features to modify. If a vector source is not provided, a layer or feature collection - * must be provided with the `layer` or `features` option. - * @property {import("../layer/BaseVector").default} [layer] The layer with - * features to modify. When provided, point features will considered for modification based on - * their visual appearance on this layer, instead of being within the `pixelTolerance` from the - * pointer location. When no `source` or `features` are configured, this layer's source will - * be used as source for modification candidates. + * features to modify. If a vector source is not provided, a feature collection + * must be provided with the `features` option. + * @property {boolean|import("../layer/BaseVector").default} [hitDetection] When configured, point + * features will considered for modification based on their visual appearance, instead of being within + * the `pixelTolerance` from the pointer location. When a {@link module:ol/layer/BaseVector} is + * provided, only the rendered representation of the features on that layer will be considered. * @property {Collection} [features] * The features the interaction works on. If a feature collection is not - * provided, a layer or vector source must be provided with the `layer` or `source` option. + * provided, a vector source must be provided with the `source` option. * @property {boolean} [wrapX=false] Wrap the world horizontally on the sketch * overlay. */ @@ -170,14 +169,13 @@ export class ModifyEvent extends Event { * `source` option. If you want to modify features in a collection (for example, * the collection used by a select interaction), construct the interaction with * the `features` option. The interaction must be constructed with either a - * `source`, `features` or `layer` option. + * `source` or `features` option. * * Cartesian distance from the pointer is used to determine the features that * will be modified. This means that geometries will only be considered for * modification when they are within the configured `pixelTolerane`. For point - * geometries, hit detection can be used to match their visual appearance. To - * enable hit detection, the interaction has to be configured with the `layer` - * that contains the points. + * geometries, the `hitDetection` option can be used to match their visual + * appearance. * * By default, the interaction will allow deletion of vertices when the `alt` * key is pressed. To configure the interaction with a different condition @@ -333,40 +331,33 @@ class Modify extends PointerInteraction { this.source_ = null; /** - * @type {import("../layer/BaseVector").default} + * @type {boolean|import("../layer/BaseVector").default} */ - this.layer_ = null; + this.hitDetection_ = null; let features; if (options.features) { features = options.features; - } else { - const source = options.source - ? options.source - : options.layer - ? options.layer.getSource() - : undefined; - if (source) { - this.source_ = source; - features = new Collection(this.source_.getFeatures()); - this.source_.addEventListener( - VectorEventType.ADDFEATURE, - this.handleSourceAdd_.bind(this) - ); - this.source_.addEventListener( - VectorEventType.REMOVEFEATURE, - this.handleSourceRemove_.bind(this) - ); - } - } - if (options.layer) { - this.layer_ = options.layer; + } else if (options.source) { + this.source_ = options.source; + features = new Collection(this.source_.getFeatures()); + this.source_.addEventListener( + VectorEventType.ADDFEATURE, + this.handleSourceAdd_.bind(this) + ); + this.source_.addEventListener( + VectorEventType.REMOVEFEATURE, + this.handleSourceRemove_.bind(this) + ); } if (!features) { throw new Error( 'The modify interaction requires features, a source or a layer' ); } + if (options.hitDetection) { + this.hitDetection_ = options.hitDetection; + } /** * @type {Collection} @@ -1123,7 +1114,11 @@ class Modify extends PointerInteraction { }; let box, hitPointGeometry; - if (this.layer_) { + if (this.hitDetection_) { + const layerFilter = + typeof this.hitDetection_ === 'object' + ? (layer) => layer === this.hitDetection_ + : undefined; map.forEachFeatureAtPixel( pixel, (feature, layer, geometry) => { @@ -1134,9 +1129,7 @@ class Modify extends PointerInteraction { } return true; }, - { - layerFilter: (layer) => layer === this.layer_, - } + {layerFilter} ); } if (!box) { diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 108a18d86c..bc7f314076 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -1,4 +1,5 @@ import Circle from '../../../../src/ol/geom/Circle.js'; +import CircleStyle from '../../../../src/ol/style/Circle.js'; import Collection from '../../../../src/ol/Collection.js'; import Event from '../../../../src/ol/events/Event.js'; import Feature from '../../../../src/ol/Feature.js'; @@ -13,6 +14,7 @@ import Snap from '../../../../src/ol/interaction/Snap.js'; import VectorLayer from '../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; import View from '../../../../src/ol/View.js'; +import {Fill, Style} from '../../../../src/ol/style.js'; import {MultiPoint} from '../../../../src/ol/geom.js'; import { clearUserProjection, @@ -22,7 +24,7 @@ import {doubleClick} from '../../../../src/ol/events/condition.js'; import {getValues} from '../../../../src/ol/obj.js'; describe('ol.interaction.Modify', function () { - let target, map, source, features; + let target, map, layer, source, features; const width = 360; const height = 180; @@ -56,7 +58,7 @@ describe('ol.interaction.Modify', function () { features: features, }); - const layer = new VectorLayer({source: source}); + layer = new VectorLayer({source: source}); map = new Map({ target: target, @@ -196,53 +198,15 @@ describe('ol.interaction.Modify', function () { expect(rbushEntries[0].feature).to.be(feature); }); - it('accepts a layer for modification features', function () { + it('accepts a hitDetection option', function () { const feature = new Feature(new Point([0, 0])); const source = new VectorSource({features: [feature]}); const layer = new VectorLayer({source: source}); - const modify = new Modify({layer: layer}); + const modify = new Modify({hitDetection: layer, source: source}); const rbushEntries = modify.rBush_.getAll(); expect(rbushEntries.length).to.be(1); expect(rbushEntries[0].feature).to.be(feature); - expect(modify.layer_).to.be(layer); - }); - - it('accepts a layer in addition to a features collection', function () { - const feature = new Feature(new Point([0, 0])); - const source = new VectorSource({features: [feature]}); - const layer = new VectorLayer({source: source}); - const features = new Collection([new Feature(new Point([1, 1]))]); - const modify = new Modify({layer: layer, features: features}); - const rbushEntries = modify.rBush_.getAll(); - expect(rbushEntries.length).to.be(1); - expect(rbushEntries[0].feature).to.be(features.item(0)); - expect(modify.layer_).to.be(layer); - }); - - it('accepts a layer in addition to a features collection', function () { - const feature = new Feature(new Point([0, 0])); - const source = new VectorSource({features: [feature]}); - const layer = new VectorLayer({source: source}); - const features = new Collection([new Feature(new Point([1, 1]))]); - const modify = new Modify({layer: layer, features: features}); - const rbushEntries = modify.rBush_.getAll(); - expect(rbushEntries.length).to.be(1); - expect(rbushEntries[0].feature).to.be(features.item(0)); - expect(modify.layer_).to.be(layer); - }); - - it('accepts a layer in addition to a source', function () { - const feature = new Feature(new Point([0, 0])); - const source = new VectorSource({features: [feature]}); - const layer = new VectorLayer({source: source}); - const candidateSource = new VectorSource({ - features: [new Feature(new Point([1, 1]))], - }); - const modify = new Modify({layer: layer, source: candidateSource}); - const rbushEntries = modify.rBush_.getAll(); - expect(rbushEntries.length).to.be(1); - expect(rbushEntries[0].feature).to.be(candidateSource.getFeatures()[0]); - expect(modify.layer_).to.be(layer); + expect(modify.hitDetection_).to.be(layer); }); }); @@ -1048,6 +1012,33 @@ describe('ol.interaction.Modify', function () { feature.getGeometry().getGeometriesArray()[1] ); }); + + it('works with hit detection of point features', function () { + const modify = new Modify({ + hitDetection: layer, + source: source, + }); + map.addInteraction(modify); + source.clear(); + const pointFeature = new Feature(new Point([0, 0])); + source.addFeature(pointFeature); + layer.setStyle( + new Style({ + image: new CircleStyle({ + radius: 30, + fill: new Fill({ + color: 'fuchsia', + }), + }), + }) + ); + map.renderSync(); + simulateEvent('pointermove', 10, -10, null, 0); + expect(modify.vertexFeature_.get('features')[0]).to.eql(pointFeature); + expect(modify.vertexFeature_.get('geometries')[0]).to.eql( + pointFeature.getGeometry() + ); + }); }); describe('#getOverlay', function () { From 18d15879bf5986b2e1dacaa1345ccbc1dac3f2e4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 1 Dec 2020 15:47:13 +0100 Subject: [PATCH 09/15] Reuse featuresBeingModified_ collection --- src/ol/interaction/Modify.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 1176dc95f5..d8d3efbeeb 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -253,7 +253,7 @@ class Modify extends PointerInteraction { this.ignoreNextSingleClick_ = false; /** - * @type {Array} + * @type {Collection} * @private */ this.featuresBeingModified_ = null; @@ -414,12 +414,12 @@ class Modify extends PointerInteraction { */ willModifyFeatures_(evt, segments) { if (!this.featuresBeingModified_) { - this.featuresBeingModified_ = []; - const features = this.featuresBeingModified_; + this.featuresBeingModified_ = new Collection(); + const features = this.featuresBeingModified_.getArray(); for (let i = 0, ii = segments.length; i < ii; ++i) { const feature = segments[i][0].feature; if (features.indexOf(feature) === -1) { - features.push(feature); + this.featuresBeingModified_.push(feature); } } @@ -1079,7 +1079,7 @@ class Modify extends PointerInteraction { this.dispatchEvent( new ModifyEvent( ModifyEventType.MODIFYEND, - new Collection(this.featuresBeingModified_), + this.featuresBeingModified_, evt ) ); @@ -1302,7 +1302,7 @@ class Modify extends PointerInteraction { this.dispatchEvent( new ModifyEvent( ModifyEventType.MODIFYEND, - new Collection(this.featuresBeingModified_), + this.featuresBeingModified_, evt ) ); From 1473731854ec3669a3b8b4e641a9503c07e460ed Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 3 Dec 2020 23:33:18 +0100 Subject: [PATCH 10/15] Use existing collection --- src/ol/interaction/Modify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index d8d3efbeeb..5c5323829f 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -426,7 +426,7 @@ class Modify extends PointerInteraction { this.dispatchEvent( new ModifyEvent( ModifyEventType.MODIFYSTART, - new Collection(features), + this.featuresBeingModified_, evt ) ); From 630a72f222212cf3d5843c10af17bd1204a045de Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 6 Dec 2020 11:21:28 +0100 Subject: [PATCH 11/15] Fix API docs Co-authored-by: MoonE --- src/ol/interaction/Modify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 5c5323829f..ee71059c5a 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -120,7 +120,7 @@ const ModifyEventType = { * features to modify. If a vector source is not provided, a feature collection * must be provided with the `features` option. * @property {boolean|import("../layer/BaseVector").default} [hitDetection] When configured, point - * features will considered for modification based on their visual appearance, instead of being within + * features will be considered for modification based on their visual appearance, instead of being within * the `pixelTolerance` from the pointer location. When a {@link module:ol/layer/BaseVector} is * provided, only the rendered representation of the features on that layer will be considered. * @property {Collection} [features] From 44e054d528e6e34fdc0ea144ae3e2d6bc4ed83cd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 6 Dec 2020 12:07:36 +0100 Subject: [PATCH 12/15] When hit detected, only consider hit node --- src/ol/interaction/Modify.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index ee71059c5a..fa4fb9dbd5 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -1113,7 +1113,7 @@ class Modify extends PointerInteraction { ); }; - let box, hitPointGeometry; + let nodes, hitPointGeometry; if (this.hitDetection_) { const layerFilter = typeof this.hitDetection_ === 'object' @@ -1125,25 +1125,32 @@ class Modify extends PointerInteraction { geometry = geometry || feature.getGeometry(); if (geometry.getType() === GeometryType.POINT) { hitPointGeometry = geometry; - box = hitPointGeometry.getExtent(); + const coordinate = geometry.getCoordinates(); + nodes = [ + { + feature, + geometry, + segment: [coordinate, coordinate], + }, + ]; } return true; }, {layerFilter} ); } - if (!box) { + if (!nodes) { const viewExtent = fromUserExtent( createExtent(pixelCoordinate, tempExtent), projection ); const buffer = map.getView().getResolution() * this.pixelTolerance_; - box = toUserExtent( + const box = toUserExtent( bufferExtent(viewExtent, buffer, tempExtent), projection ); + nodes = this.rBush_.getInExtent(box); } - const nodes = this.rBush_.getInExtent(box); if (nodes && nodes.length > 0) { const node = nodes.sort(sortByDistance)[0]; From 0e15720f2eca0508f5571a8bd35d960164bb6ef4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 6 Dec 2020 17:02:04 +0100 Subject: [PATCH 13/15] Make use of the Modify interaction's overlay source --- examples/modify-icon.js | 7 +++---- src/ol/interaction/Modify.js | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/modify-icon.js b/examples/modify-icon.js index 0efee5e415..b89971e5fa 100644 --- a/examples/modify-icon.js +++ b/examples/modify-icon.js @@ -58,10 +58,9 @@ const modify = new Modify({ modify.on(['modifystart', 'modifyend'], function (evt) { target.style.cursor = evt.type === 'modifystart' ? 'grabbing' : 'pointer'; }); -map.on('pointermove', function (evt) { - if (!evt.dragging) { - target.style.cursor = map.hasFeatureAtPixel(evt.pixel) ? 'pointer' : ''; - } +const overlaySource = modify.getOverlay().getSource(); +overlaySource.on(['addfeature', 'removefeature'], function (evt) { + target.style.cursor = evt.type === 'addfeature' ? 'pointer' : ''; }); map.addInteraction(modify); diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index fa4fb9dbd5..573f1ddbd7 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -108,7 +108,7 @@ const ModifyEventType = { * @property {number} [pixelTolerance=10] Pixel tolerance for considering the * pointer close enough to a segment or vertex for editing. * @property {import("../style/Style.js").StyleLike} [style] - * Style used for the modification point. For linestrings and polygons, this will + * Style used for the modification point or vertex. For linestrings and polygons, this will * be the affected vertex, for circles a point along the circle, and for points the actual * point. If not configured, the default edit style is used (see {@link module:ol/style}). * When using a style function, the point feature passed to the function will have a `features` @@ -505,7 +505,7 @@ class Modify extends PointerInteraction { } /** - * Get the overlay layer that this interaction renders sketch features to. + * Get the overlay layer that this interaction renders the modification point or vertex to. * @return {VectorLayer} Overlay layer. * @api */ From b6eb429f7775c09d2970e6041b8c618c074eed44 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 6 Dec 2020 17:47:19 +0100 Subject: [PATCH 14/15] Always drag using pointer location, not vertex location --- src/ol/interaction/Modify.js | 4 ++-- test/spec/ol/interaction/modify.test.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index 573f1ddbd7..58b6178553 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -1163,8 +1163,8 @@ class Modify extends PointerInteraction { const vertexSegments = {}; vertexSegments[getUid(closestSegment)] = true; - this.delta_[0] = hitPointGeometry ? vertex[0] - pixelCoordinate[0] : 0; - this.delta_[1] = hitPointGeometry ? vertex[1] - pixelCoordinate[1] : 0; + this.delta_[0] = vertex[0] - pixelCoordinate[0]; + this.delta_[1] = vertex[1] - pixelCoordinate[1]; if ( node.geometry.getType() === GeometryType.CIRCLE && node.index === CIRCLE_CIRCUMFERENCE_INDEX diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index bc7f314076..386869d593 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -498,7 +498,7 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointerdrag', 30, -5, null, 0); simulateEvent('pointerup', 30, -5, null, 0); - expect(circleFeature.getGeometry().getRadius()).to.equal(25); + expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 0.1); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); // Increase radius along y axis @@ -508,7 +508,7 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointerdrag', 5, -35, null, 0); simulateEvent('pointerup', 5, -35, null, 0); - expect(circleFeature.getGeometry().getRadius()).to.equal(30); + expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(30, 0.1); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); }); @@ -553,7 +553,7 @@ describe('ol.interaction.Modify', function () { .getGeometry() .clone() .transform(userProjection, viewProjection); - expect(geometry2.getRadius()).to.roughlyEqual(25, 1e-9); + expect(geometry2.getRadius()).to.roughlyEqual(25, 0.1); expect(geometry2.getCenter()).to.eql([5, 5]); // Increase radius along y axis @@ -567,7 +567,7 @@ describe('ol.interaction.Modify', function () { .getGeometry() .clone() .transform(userProjection, viewProjection); - expect(geometry3.getRadius()).to.roughlyEqual(30, 1e-9); + expect(geometry3.getRadius()).to.roughlyEqual(30, 0.1); expect(geometry3.getCenter()).to.eql([5, 5]); }); }); @@ -1084,7 +1084,7 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointerdrag', 30, -5, null, 0); simulateEvent('pointerup', 30, -5, null, 0); - expect(circleFeature.getGeometry().getRadius()).to.equal(25); + expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 1e9); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); // Increase radius along y axis From ed3c45d3f8474068571185fb68fd6b7838c025cd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 6 Dec 2020 22:12:13 +0100 Subject: [PATCH 15/15] Fix tolerance typo --- test/spec/ol/interaction/modify.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 386869d593..f6387f5b57 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -1084,7 +1084,7 @@ describe('ol.interaction.Modify', function () { simulateEvent('pointerdrag', 30, -5, null, 0); simulateEvent('pointerup', 30, -5, null, 0); - expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 1e9); + expect(circleFeature.getGeometry().getRadius()).to.roughlyEqual(25, 1e-9); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); // Increase radius along y axis