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 () {