UNPKG

ol

Version:

OpenLayers mapping library

1,607 lines (1,478 loc) • 76.1 kB
/** * @module ol/interaction/Modify */ import Collection from '../Collection.js'; import CollectionEventType from '../CollectionEventType.js'; import Feature from '../Feature.js'; import MapBrowserEventType from '../MapBrowserEventType.js'; import ObjectEventType from '../ObjectEventType.js'; import {equals} from '../array.js'; import { closestOnSegment, distance as coordinateDistance, equals as coordinatesEqual, squaredDistance as squaredCoordinateDistance, squaredDistanceToSegment, } from '../coordinate.js'; import Event from '../events/Event.js'; import EventType from '../events/EventType.js'; import { altKeyOnly, always, never, primaryAction, singleClick, } from '../events/condition.js'; import { boundingExtent, buffer as bufferExtent, createOrUpdateFromCoordinate as createExtent, } from '../extent.js'; import Point from '../geom/Point.js'; import {fromCircle} from '../geom/Polygon.js'; import VectorLayer from '../layer/Vector.js'; import { fromUserCoordinate, fromUserExtent, getUserProjection, toUserCoordinate, toUserExtent, } from '../proj.js'; import VectorSource from '../source/Vector.js'; import VectorEventType from '../source/VectorEventType.js'; import RBush from '../structs/RBush.js'; import {createEditingStyle} from '../style/Style.js'; import {getUid} from '../util.js'; import PointerInteraction from './Pointer.js'; import { getCoordinate, getTraceTargetUpdate, getTraceTargets, } from './tracing.js'; /** * The segment index assigned to a circle's center when * breaking up a circle into ModifySegmentDataType segments. * @type {number} */ const CIRCLE_CENTER_INDEX = 0; /** * The segment index assigned to a circle's circumference when * breaking up a circle into ModifySegmentDataType segments. * @type {number} */ const CIRCLE_CIRCUMFERENCE_INDEX = 1; const tempExtent = [0, 0, 0, 0]; const tempSegment = []; /** * @enum {string} */ const ModifyEventType = { /** * Triggered upon feature modification start * @event ModifyEvent#modifystart * @api */ MODIFYSTART: 'modifystart', /** * Triggered upon feature modification end * @event ModifyEvent#modifyend * @api */ MODIFYEND: 'modifyend', }; /** * @typedef {Object} SegmentData * @property {Array<number>} [depth] Depth. * @property {Feature} feature Feature. * @property {import("../geom/SimpleGeometry.js").default} geometry Geometry. * @property {number} [index] Index. * @property {Array<Array<number>>} segment Segment. * @property {Array<SegmentData>} [featureSegments] FeatureSegments. */ /** * A function that takes a {@link module:ol/Feature~Feature} and returns `true` if * the feature may be modified or `false` otherwise. * @typedef {function(Feature):boolean} FilterFunction */ /** * @typedef {[SegmentData, number]} DragSegment */ /** * @typedef {Object} Options * @property {import("../events/condition.js").Condition} [condition] A function that * takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a * boolean to indicate whether that event will be considered to add or move a * vertex to the sketch. Default is * {@link module:ol/events/condition.primaryAction}. * @property {import("../events/condition.js").Condition} [deleteCondition] A function * that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and returns a * boolean to indicate whether that event should be handled. By default, * {@link module:ol/events/condition.singleClick} with * {@link module:ol/events/condition.altKeyOnly} results in a vertex deletion. * This combination is handled by wrapping the two condition checks in a single function: * ```js * import { altKeyOnly, singleClick } from 'ol/events/condition.js'; * * function (event) { * return altKeyOnly(event) && singleClick(event) * } * ``` * @property {import("../events/condition.js").Condition} [insertVertexCondition] A * function that takes a {@link module:ol/MapBrowserEvent~MapBrowserEvent} and * returns a boolean to indicate whether a new vertex should be added to the sketch * features. Default is {@link module:ol/events/condition.always}. * @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|import("../style/flat.js").FlatStyleLike} [style] * 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/Style~Style}). * When using a style function, the point feature passed to the function will have an `existing` property - * indicating whether there is an existing vertex underneath or not, a `features` * 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 feature collection * must be provided with the `features` option. * @property {boolean|import("../layer/BaseVector").default} [hitDetection] When configured, point * 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~BaseVectorLayer} is * provided, only the rendered representation of the features on that layer will be considered. * @property {Collection<Feature>} [features] * The features the interaction works on. If a feature collection is not * provided, a vector source must be provided with the `source` option. * @property {FilterFunction} [filter] A function that takes a {@link module:ol/Feature~Feature} * and returns `true` if the feature may be modified or `false` otherwise. * @property {boolean|import("../events/condition.js").Condition} [trace=false] Trace a portion of another geometry. * Tracing starts when two neighboring vertices are dragged onto a trace target, without any other modification in between.. * @property {VectorSource} [traceSource] Source for features to trace. If tracing is active and a `traceSource` is * not provided, the interaction's `source` will be used. Tracing requires that the interaction is configured with * either a `traceSource` or a `source`. * @property {boolean} [wrapX=false] Wrap the world horizontally on the sketch * overlay. * @property {boolean} [snapToPointer=!hitDetection] The vertex, point or segment being modified snaps to the * pointer coordinate when clicked within the `pixelTolerance`. */ function getCoordinatesArray(coordinates, geometryType, depth) { let coordinatesArray; switch (geometryType) { case 'LineString': coordinatesArray = coordinates; break; case 'MultiLineString': case 'Polygon': coordinatesArray = coordinates[depth[0]]; break; case 'MultiPolygon': coordinatesArray = coordinates[depth[1]][depth[0]]; break; default: // pass } return coordinatesArray; } /** * @classdesc * Events emitted by {@link module:ol/interaction/Modify~Modify} instances are * instances of this type. */ export class ModifyEvent extends Event { /** * @param {ModifyEventType} type Type. * @param {Collection<Feature>} features * The features modified. * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent * Associated {@link module:ol/MapBrowserEvent~MapBrowserEvent}. */ constructor(type, features, mapBrowserEvent) { super(type); /** * The features being modified. * @type {Collection<Feature>} * @api */ this.features = features; /** * Associated {@link module:ol/MapBrowserEvent~MapBrowserEvent}. * @type {import("../MapBrowserEvent.js").default} * @api */ this.mapBrowserEvent = mapBrowserEvent; } } /*** * @template Return * @typedef {import("../Observable").OnSignature<import("../Observable").EventTypes, import("../events/Event.js").default, Return> & * import("../Observable").OnSignature<import("../ObjectEventType").Types| * 'change:active', import("../Object").ObjectEvent, Return> & * import("../Observable").OnSignature<'modifyend'|'modifystart', ModifyEvent, Return> & * import("../Observable").CombinedOnSignature<import("../Observable").EventTypes|import("../ObjectEventType").Types| * 'change:active'|'modifyend'|'modifystart', Return>} ModifyOnSignature */ /** * @classdesc * Interaction for modifying feature geometries. To modify features that have * been added to an existing source, construct the modify interaction with the * `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. * * 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 `pixelTolerance`. For point * 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 * for deletion, use the `deleteCondition` option. * @fires ModifyEvent * @api */ class Modify extends PointerInteraction { /** * @param {Options} options Options. */ constructor(options) { super(/** @type {import("./Pointer.js").Options} */ (options)); //Maintain a ref to event handlers for later unregistering /** @private */ this.handleSourceAdd_ = this.handleSourceAdd_.bind(this); /** @private */ this.handleSourceRemove_ = this.handleSourceRemove_.bind(this); /** @private */ this.handleExternalCollectionAdd_ = this.handleExternalCollectionAdd_.bind(this); /** @private */ this.handleExternalCollectionRemove_ = this.handleExternalCollectionRemove_.bind(this); /** @private */ this.handleFeatureChange_ = this.handleFeatureChange_.bind(this); /*** * @type {ModifyOnSignature<import("../events").EventsKey>} */ this.on; /*** * @type {ModifyOnSignature<import("../events").EventsKey>} */ this.once; /*** * @type {ModifyOnSignature<void>} */ this.un; /** * @private * @type {import("../events/condition.js").Condition} */ this.condition_ = options.condition ? options.condition : primaryAction; /** * @private * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Browser event. * @return {boolean} Combined condition result. */ this.defaultDeleteCondition_ = function (mapBrowserEvent) { return altKeyOnly(mapBrowserEvent) && singleClick(mapBrowserEvent); }; /** * @type {import("../events/condition.js").Condition} * @private */ this.deleteCondition_ = options.deleteCondition ? options.deleteCondition : this.defaultDeleteCondition_; /** * @type {import("../events/condition.js").Condition} * @private */ this.insertVertexCondition_ = options.insertVertexCondition ? options.insertVertexCondition : always; /** * Editing vertex. * @type {Feature<Point>} * @private */ this.vertexFeature_ = null; /** * Segments intersecting {@link this.vertexFeature_} by segment uid. * @type {Object<string, boolean>} * @private */ this.vertexSegments_ = null; /** * @type {import("../coordinate.js").Coordinate} * @private */ this.lastCoordinate_ = [0, 0]; /** * Tracks if the next `singleclick` event should be ignored to prevent * accidental deletion right after vertex creation. * @type {boolean} * @private */ this.ignoreNextSingleClick_ = false; /** * @type {Collection<Feature>} * @private */ this.featuresBeingModified_ = null; /** * Segment RTree for each layer * @type {RBush<SegmentData>} * @private */ this.rBush_ = new RBush(); /** * @type {number} * @private */ this.pixelTolerance_ = options.pixelTolerance !== undefined ? options.pixelTolerance : 10; /** * @type {boolean} * @private */ this.snappedToVertex_ = false; /** * Indicate whether the interaction is currently changing a feature's * coordinates. * @type {boolean} * @private */ this.changingFeature_ = false; /** * @type {Array<DragSegment>} * @private */ this.dragSegments_ = []; /** * Draw overlay where sketch features are drawn. * @type {VectorLayer} * @private */ this.overlay_ = new VectorLayer({ source: new VectorSource({ useSpatialIndex: false, wrapX: !!options.wrapX, }), style: options.style ? options.style : getDefaultStyleFunction(), updateWhileAnimating: true, updateWhileInteracting: true, }); /** * @const * @private * @type {!Object<string, function(Feature, import("../geom/Geometry.js").default): void>} */ this.SEGMENT_WRITERS_ = { Point: this.writePointGeometry_.bind(this), LineString: this.writeLineStringGeometry_.bind(this), LinearRing: this.writeLineStringGeometry_.bind(this), Polygon: this.writePolygonGeometry_.bind(this), MultiPoint: this.writeMultiPointGeometry_.bind(this), MultiLineString: this.writeMultiLineStringGeometry_.bind(this), MultiPolygon: this.writeMultiPolygonGeometry_.bind(this), Circle: this.writeCircleGeometry_.bind(this), GeometryCollection: this.writeGeometryCollectionGeometry_.bind(this), }; /** * @type {VectorSource} * @private */ this.source_ = null; /** * @type {VectorSource|null} * @private */ this.traceSource_ = options.traceSource || options.source || null; /** * @type {import("../events/condition.js").Condition} * @private */ this.traceCondition_; this.setTrace(options.trace || false); /** * @type {import('./tracing.js').TraceState} * @private */ this.traceState_ = {active: false}; /** * @type {Array<DragSegment>|null} * @private */ this.traceSegments_ = null; /** * @type {boolean|import("../layer/BaseVector").default} * @private */ this.hitDetection_ = null; /** * Useful for performance optimization * @private * @type boolean */ this.filterFunctionWasSupplied_ = options.filter != undefined ? true : false; /** * @private * @type {FilterFunction} */ this.filter_ = options.filter ? options.filter : () => true; if (!(options.features || options.source)) { throw new Error( 'The modify interaction requires features collection or a source', ); } /** @type {Array<Feature>} */ let features; if (options.features) { features = options.features.getArray(); //setup listeners on external features collection and features options.features.addEventListener( CollectionEventType.ADD, this.handleExternalCollectionAdd_, ); options.features.addEventListener( CollectionEventType.REMOVE, this.handleExternalCollectionRemove_, ); //keep ref for unsubscribe on dispose this.featuresCollection_ = options.features; } else if (options.source) { features = options.source.getFeatures(); //setup listeners on external source and features options.source.addEventListener( VectorEventType.ADDFEATURE, this.handleSourceAdd_, ); options.source.addEventListener( VectorEventType.REMOVEFEATURE, this.handleSourceRemove_, ); //keep ref for unsubscribe on dispose this.source_ = options.source; } features.forEach((feature) => { //any modification to the feature requires filter to be re-run feature.addEventListener(EventType.CHANGE, this.handleFeatureChange_); //prop change handler is only to re-run the filter if (this.filterFunctionWasSupplied_) { feature.addEventListener( ObjectEventType.PROPERTYCHANGE, this.handleFeatureChange_, ); } }); if (options.hitDetection) { this.hitDetection_ = options.hitDetection; } /** * Internal features array. When adding or removing features, be sure to use * addFeature_()/removeFeature_() so that the the segment index is adjusted. * @type {Array<Feature>} * @private */ this.features_ = []; features .filter(this.filter_) .forEach((feature) => this.addFeature_(feature)); /** * @type {import("../MapBrowserEvent.js").default} * @private */ this.lastPointerEvent_ = null; /** * Delta (x, y in map units) between matched rtree vertex and pointer vertex. * @type {Array<number>} * @private */ this.delta_ = [0, 0]; /** * @private */ this.snapToPointer_ = options.snapToPointer === undefined ? !this.hitDetection_ : options.snapToPointer; } /** * Toggle tracing mode or set a tracing condition. * * @param {boolean|import("../events/condition.js").Condition} trace A boolean to toggle tracing mode or an event * condition that will be checked when a feature is clicked to determine if tracing should be active. */ setTrace(trace) { let condition; if (!trace) { condition = never; } else if (trace === true) { condition = always; } else { condition = trace; } this.traceCondition_ = condition; } /** * Called when a feature is added to the internal features collection * @param {Feature} feature Feature. * @private */ addFeature_(feature) { this.features_.push(feature); const geometry = feature.getGeometry(); if (geometry) { const writer = this.SEGMENT_WRITERS_[geometry.getType()]; if (writer) { writer(feature, geometry); } } const map = this.getMap(); if (map && map.isRendered() && this.getActive()) { this.handlePointerAtPixel_(this.lastCoordinate_); } } /** * @param {import("../MapBrowserEvent.js").default} evt Map browser event. * @param {Array<SegmentData>} segments The segments subject to modification. * @private */ willModifyFeatures_(evt, segments) { if (!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].feature; if (feature && !features.includes(feature)) { this.featuresBeingModified_.push(feature); } } if (this.featuresBeingModified_.getLength() === 0) { this.featuresBeingModified_ = null; } else { this.dispatchEvent( new ModifyEvent( ModifyEventType.MODIFYSTART, this.featuresBeingModified_, evt, ), ); } } } /** * Removes a feature from the internal features collection and updates internal state * accordingly. * @param {Feature} feature Feature. * @private */ removeFeature_(feature) { const itemIndex = this.features_.indexOf(feature); this.features_.splice(itemIndex, 1); this.removeFeatureSegmentData_(feature); // Remove the vertex feature if the collection of candidate features is empty. if (this.vertexFeature_ && this.features_.length === 0) { this.overlay_.getSource().removeFeature(this.vertexFeature_); this.vertexFeature_ = null; } } /** * @param {Feature} feature Feature. * @private */ removeFeatureSegmentData_(feature) { const rBush = this.rBush_; /** @type {Array<SegmentData>} */ const nodesToRemove = []; rBush.forEach( /** * @param {SegmentData} node RTree node. */ function (node) { if (feature === node.feature) { nodesToRemove.push(node); } }, ); for (let i = nodesToRemove.length - 1; i >= 0; --i) { const nodeToRemove = nodesToRemove[i]; for (let j = this.dragSegments_.length - 1; j >= 0; --j) { if (this.dragSegments_[j][0] === nodeToRemove) { this.dragSegments_.splice(j, 1); } } rBush.remove(nodeToRemove); } } /** * Activate or deactivate the interaction. * @param {boolean} active Active. * @observable * @api * @override */ setActive(active) { if (this.vertexFeature_ && !active) { this.overlay_.getSource().removeFeature(this.vertexFeature_); this.vertexFeature_ = null; } super.setActive(active); } /** * Remove the interaction from its current map and attach it to the new map. * Subclasses may set up event handlers to get notified about changes to * the map here. * @param {import("../Map.js").default} map Map. * @override */ setMap(map) { this.overlay_.setMap(map); super.setMap(map); } /** * Get the overlay layer that this interaction renders the modification point or vertex to. * @return {VectorLayer} Overlay layer. * @api */ getOverlay() { return this.overlay_; } /** * @param {import("../source/Vector.js").VectorSourceEvent} event Event. * @private */ handleSourceAdd_(event) { const feature = event.feature; if (feature) { this.externalAddFeatureHandler_(feature); } } /** * @param {import("../source/Vector.js").VectorSourceEvent} event Event. * @private */ handleSourceRemove_(event) { const feature = event.feature; if (feature) { this.externalRemoveFeatureHandler_(feature); } } /** * @param {import("../Collection.js").CollectionEvent} event Event. * @private */ handleExternalCollectionAdd_(event) { const feature = event.element; if (feature) { this.externalAddFeatureHandler_(feature); } } /** * @param {import("../Collection.js").CollectionEvent} event Event. * @private */ handleExternalCollectionRemove_(event) { const feature = event.element; if (feature) { this.externalRemoveFeatureHandler_(feature); } } /** * Common handler for event signaling addition of feature to the supplied features source * or collection. * @param {Feature} feature Feature. */ externalAddFeatureHandler_(feature) { feature.addEventListener(EventType.CHANGE, this.handleFeatureChange_); //prop change handler is only for reapplying the filter if (this.filterFunctionWasSupplied_) { feature.addEventListener( ObjectEventType.PROPERTYCHANGE, this.handleFeatureChange_, ); } if (this.filter_(feature)) { this.addFeature_(feature); } } /** * Common handler for event signaling removal of feature from the supplied features source * or collection. * @param {Feature} feature Feature. */ externalRemoveFeatureHandler_(feature) { feature.removeEventListener(EventType.CHANGE, this.handleFeatureChange_); if (this.filterFunctionWasSupplied_) { feature.removeEventListener( ObjectEventType.PROPERTYCHANGE, this.handleFeatureChange_, ); } this.removeFeature_(feature); } /** * Listener for features in external source or features collection. Ensures the feature filter * is re-run and segment data is updated. * @param {import("../events/Event.js").default | import("../Object").ObjectEvent} evt Event. * @private */ handleFeatureChange_(evt) { if (!this.changingFeature_) { const feature = /** @type {Feature} */ (evt.target); this.removeFeature_(feature); //safe to remove handler on a feature if there isn't one, but need to apply the filter // before adding the feature. this.filter_(feature) && this.addFeature_(feature); } } /** * @param {Feature} feature Feature * @param {Point} geometry Geometry. * @private */ writePointGeometry_(feature, geometry) { const coordinates = geometry.getCoordinates(); /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, segment: [coordinates, coordinates], }; this.rBush_.insert(geometry.getExtent(), segmentData); } /** * @param {Feature} feature Feature * @param {import("../geom/MultiPoint.js").default} geometry Geometry. * @private */ writeMultiPointGeometry_(feature, geometry) { const points = geometry.getCoordinates(); for (let i = 0, ii = points.length; i < ii; ++i) { const coordinates = points[i]; /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, depth: [i], index: i, segment: [coordinates, coordinates], }; this.rBush_.insert(geometry.getExtent(), segmentData); } } /** * @param {Feature} feature Feature * @param {import("../geom/LineString.js").default} geometry Geometry. * @private */ writeLineStringGeometry_(feature, geometry) { const coordinates = geometry.getCoordinates(); for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, index: i, segment: segment, }; this.rBush_.insert(boundingExtent(segment), segmentData); } } /** * @param {Feature} feature Feature * @param {import("../geom/MultiLineString.js").default} geometry Geometry. * @private */ writeMultiLineStringGeometry_(feature, geometry) { const lines = geometry.getCoordinates(); for (let j = 0, jj = lines.length; j < jj; ++j) { const coordinates = lines[j]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, depth: [j], index: i, segment: segment, }; this.rBush_.insert(boundingExtent(segment), segmentData); } } } /** * @param {Feature} feature Feature * @param {import("../geom/Polygon.js").default} geometry Geometry. * @private */ writePolygonGeometry_(feature, geometry) { const rings = geometry.getCoordinates(); for (let j = 0, jj = rings.length; j < jj; ++j) { const coordinates = rings[j]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, depth: [j], index: i, segment: segment, }; this.rBush_.insert(boundingExtent(segment), segmentData); } } } /** * @param {Feature} feature Feature * @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @private */ writeMultiPolygonGeometry_(feature, geometry) { const polygons = geometry.getCoordinates(); for (let k = 0, kk = polygons.length; k < kk; ++k) { const rings = polygons[k]; for (let j = 0, jj = rings.length; j < jj; ++j) { const coordinates = rings[j]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); /** @type {SegmentData} */ const segmentData = { feature: feature, geometry: geometry, depth: [j, k], index: i, segment: segment, }; this.rBush_.insert(boundingExtent(segment), segmentData); } } } } /** * We convert a circle into two segments. The segment at index * {@link CIRCLE_CENTER_INDEX} is the * circle's center (a point). The segment at index * {@link CIRCLE_CIRCUMFERENCE_INDEX} is * the circumference, and is not a line segment. * * @param {Feature} feature Feature. * @param {import("../geom/Circle.js").default} geometry Geometry. * @private */ writeCircleGeometry_(feature, geometry) { const coordinates = geometry.getCenter(); /** @type {SegmentData} */ const centerSegmentData = { feature: feature, geometry: geometry, index: CIRCLE_CENTER_INDEX, segment: [coordinates, coordinates], }; /** @type {SegmentData} */ const circumferenceSegmentData = { feature: feature, geometry: geometry, index: CIRCLE_CIRCUMFERENCE_INDEX, segment: [coordinates, coordinates], }; const featureSegments = [centerSegmentData, circumferenceSegmentData]; centerSegmentData.featureSegments = featureSegments; circumferenceSegmentData.featureSegments = featureSegments; this.rBush_.insert(createExtent(coordinates), centerSegmentData); let circleGeometry = /** @type {import("../geom/Geometry.js").default} */ ( geometry ); const userProjection = getUserProjection(); if (userProjection && this.getMap()) { const projection = this.getMap().getView().getProjection(); circleGeometry = circleGeometry .clone() .transform(userProjection, projection); circleGeometry = fromCircle( /** @type {import("../geom/Circle.js").default} */ (circleGeometry), ).transform(projection, userProjection); } this.rBush_.insert(circleGeometry.getExtent(), circumferenceSegmentData); } /** * @param {Feature} feature Feature * @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @private */ writeGeometryCollectionGeometry_(feature, geometry) { const geometries = geometry.getGeometriesArray(); for (let i = 0; i < geometries.length; ++i) { const geometry = geometries[i]; const writer = this.SEGMENT_WRITERS_[geometry.getType()]; writer(feature, geometry); } } /** * @param {import("../coordinate.js").Coordinate} coordinates Coordinates. * @param {Array<Feature>} features The features being modified. * @param {Array<import("../geom/SimpleGeometry.js").default>} geometries The geometries being modified. * @param {boolean} existing The vertex represents an existing vertex. * @return {Feature} Vertex feature. * @private */ createOrUpdateVertexFeature_(coordinates, features, geometries, existing) { let vertexFeature = this.vertexFeature_; if (!vertexFeature) { vertexFeature = new Feature(new Point(coordinates)); this.vertexFeature_ = vertexFeature; this.overlay_.getSource().addFeature(vertexFeature); } else { const geometry = vertexFeature.getGeometry(); geometry.setCoordinates(coordinates); } vertexFeature.set('features', features); vertexFeature.set('geometries', geometries); vertexFeature.set('existing', existing); return vertexFeature; } /** * Handles the {@link module:ol/MapBrowserEvent~MapBrowserEvent map browser event} and may modify the geometry. * @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event. * @return {boolean} `false` to stop event propagation. * @override */ handleEvent(mapBrowserEvent) { if (!mapBrowserEvent.originalEvent) { return true; } this.lastPointerEvent_ = mapBrowserEvent; let handled; if ( !mapBrowserEvent.map.getView().getInteracting() && mapBrowserEvent.type == MapBrowserEventType.POINTERMOVE && !this.handlingDownUpSequence ) { this.handlePointerMove_(mapBrowserEvent); } if (this.vertexFeature_ && this.deleteCondition_(mapBrowserEvent)) { if ( mapBrowserEvent.type != MapBrowserEventType.SINGLECLICK || !this.ignoreNextSingleClick_ ) { handled = this.removePoint(); } else { handled = true; } } if (mapBrowserEvent.type == MapBrowserEventType.SINGLECLICK) { this.ignoreNextSingleClick_ = false; } return super.handleEvent(mapBrowserEvent) && !handled; } /** * @param {import("../coordinate.js").Coordinate} pixelCoordinate Pixel coordinate. * @return {Array<SegmentData>|undefined} Insert vertices and update drag segments. * @private */ findInsertVerticesAndUpdateDragSegments_(pixelCoordinate) { this.handlePointerAtPixel_(pixelCoordinate); this.dragSegments_.length = 0; this.featuresBeingModified_ = null; const vertexFeature = this.vertexFeature_; if (!vertexFeature) { return; } const projection = this.getMap().getView().getProjection(); /** @type {Array<SegmentData>} */ const insertVertices = []; const vertex = this.vertexFeature_.getGeometry().getCoordinates(); const vertexExtent = boundingExtent([vertex]); const segmentDataMatches = this.rBush_.getInExtent(vertexExtent); const componentSegments = {}; segmentDataMatches.sort(compareIndexes); for (let i = 0, ii = segmentDataMatches.length; i < ii; ++i) { const segmentDataMatch = segmentDataMatches[i]; const segment = segmentDataMatch.segment; let uid = getUid(segmentDataMatch.geometry); const depth = segmentDataMatch.depth; if (depth) { uid += '-' + depth.join('-'); // separate feature components } if (!componentSegments[uid]) { componentSegments[uid] = new Array(2); } if ( segmentDataMatch.geometry.getType() === 'Circle' && segmentDataMatch.index === CIRCLE_CIRCUMFERENCE_INDEX ) { const closestVertex = closestOnSegmentData( pixelCoordinate, segmentDataMatch, projection, ); if ( coordinatesEqual(closestVertex, vertex) && !componentSegments[uid][0] ) { this.dragSegments_.push([segmentDataMatch, 0]); componentSegments[uid][0] = segmentDataMatch; } continue; } if (coordinatesEqual(segment[0], vertex) && !componentSegments[uid][0]) { this.dragSegments_.push([segmentDataMatch, 0]); componentSegments[uid][0] = segmentDataMatch; continue; } if (coordinatesEqual(segment[1], vertex) && !componentSegments[uid][1]) { if ( componentSegments[uid][0] && componentSegments[uid][0].index === 0 ) { let coordinates = segmentDataMatch.geometry.getCoordinates(); switch (segmentDataMatch.geometry.getType()) { // prevent dragging closed linestrings by the connecting node case 'LineString': case 'MultiLineString': continue; // if dragging the first vertex of a polygon, ensure the other segment // belongs to the closing vertex of the linear ring case 'MultiPolygon': coordinates = coordinates[depth[1]]; /* falls through */ case 'Polygon': if (segmentDataMatch.index !== coordinates[depth[0]].length - 2) { continue; } break; default: // pass } } this.dragSegments_.push([segmentDataMatch, 1]); componentSegments[uid][1] = segmentDataMatch; continue; } if ( getUid(segment) in this.vertexSegments_ && !componentSegments[uid][0] && !componentSegments[uid][1] ) { insertVertices.push(segmentDataMatch); } } return insertVertices; } /** * @private */ deactivateTrace_() { this.traceState_ = {active: false}; } /** * Update the trace. * @param {import("../MapBrowserEvent.js").default} event Event. * @private */ updateTrace_(event) { const traceState = this.traceState_; if (!traceState.active) { return; } if (traceState.targetIndex === -1) { // check if we are ready to pick a target const startPx = event.map.getPixelFromCoordinate(traceState.startCoord); if (coordinateDistance(startPx, event.pixel) < this.pixelTolerance_) { return; } } const updatedTraceTarget = getTraceTargetUpdate( event.coordinate, traceState, event.map, this.pixelTolerance_, ); if ( traceState.targetIndex === -1 && Math.sqrt(updatedTraceTarget.closestTargetDistance) / event.map.getView().getResolution() > this.pixelTolerance_ ) { return; } if (traceState.targetIndex !== updatedTraceTarget.index) { // target changed if (traceState.targetIndex !== -1) { // remove points added during previous trace const oldTarget = traceState.targets[traceState.targetIndex]; this.removeTracedCoordinates_(oldTarget.startIndex, oldTarget.endIndex); } else { for (const traceSegment of this.traceSegments_) { const segmentData = traceSegment[0]; const geometry = segmentData.geometry; const index = traceSegment[1]; const coordinates = geometry.getCoordinates(); const coordinatesArray = getCoordinatesArray( coordinates, geometry.getType(), segmentData.depth, ); coordinatesArray.splice(segmentData.index + index, 1); geometry.setCoordinates(coordinates); if (index === 0) { segmentData.index -= 1; } } } // add points for the new target const newTarget = traceState.targets[updatedTraceTarget.index]; this.addTracedCoordinates_( newTarget, newTarget.startIndex, updatedTraceTarget.endIndex, ); } else { // target stayed the same const target = traceState.targets[traceState.targetIndex]; this.addOrRemoveTracedCoordinates_(target, updatedTraceTarget.endIndex); } // modify the state with updated info traceState.targetIndex = updatedTraceTarget.index; const target = traceState.targets[traceState.targetIndex]; target.endIndex = updatedTraceTarget.endIndex; } getTraceCandidates_(event) { const map = this.getMap(); const tolerance = this.pixelTolerance_; const lowerLeft = map.getCoordinateFromPixel([ event.pixel[0] - tolerance, event.pixel[1] + tolerance, ]); const upperRight = map.getCoordinateFromPixel([ event.pixel[0] + tolerance, event.pixel[1] - tolerance, ]); const extent = boundingExtent([lowerLeft, upperRight]); const features = this.traceSource_.getFeaturesInExtent(extent); return features; } /** * Activate or deactivate trace state based on a browser event. * @param {import("../MapBrowserEvent.js").default} event Event. * @private */ toggleTraceState_(event) { if (!this.traceSource_ || !this.traceCondition_(event)) { return; } if (this.traceState_.active) { this.deactivateTrace_(); this.traceSegments_ = null; return; } const features = this.getTraceCandidates_(event); if (features.length === 0) { return; } const targets = getTraceTargets(event.coordinate, features); if (targets.length) { this.traceState_ = { active: true, startCoord: event.coordinate.slice(), targets: targets, targetIndex: -1, }; } } /** * @param {import('./tracing.js').TraceTarget} target The trace target. * @param {number} endIndex The new end index of the trace. * @private */ addOrRemoveTracedCoordinates_(target, endIndex) { // three cases to handle: // 1. traced in the same direction and points need adding // 2. traced in the same direction and points need removing // 3. traced in a new direction const previouslyForward = target.startIndex <= target.endIndex; const currentlyForward = target.startIndex <= endIndex; if (previouslyForward === currentlyForward) { // same direction if ( (previouslyForward && endIndex > target.endIndex) || (!previouslyForward && endIndex < target.endIndex) ) { // case 1 - add new points this.addTracedCoordinates_(target, target.endIndex, endIndex); } else if ( (previouslyForward && endIndex < target.endIndex) || (!previouslyForward && endIndex > target.endIndex) ) { // case 2 - remove old points this.removeTracedCoordinates_(endIndex, target.endIndex); } } else { // case 3 - remove old points, add new points this.removeTracedCoordinates_(target.startIndex, target.endIndex); this.addTracedCoordinates_(target, target.startIndex, endIndex); } } /** * @param {number} fromIndex The start index. * @param {number} toIndex The end index. * @private */ removeTracedCoordinates_(fromIndex, toIndex) { if (fromIndex === toIndex) { return; } let remove = 0; if (fromIndex < toIndex) { const start = Math.ceil(fromIndex); let end = Math.floor(toIndex); if (end === toIndex) { end -= 1; } remove = end - start + 1; } else { const start = Math.floor(fromIndex); let end = Math.ceil(toIndex); if (end === toIndex) { end += 1; } remove = start - end + 1; } if (remove > 0) { for (const traceSegment of this.traceSegments_) { const segmentData = traceSegment[0]; const geometry = segmentData.geometry; const index = traceSegment[1]; let removeIndex = traceSegment[0].index + 1; if (index === 1) { removeIndex -= remove; } const coordinates = geometry.getCoordinates(); const coordinatesArray = getCoordinatesArray( coordinates, geometry.getType(), segmentData.depth, ); coordinatesArray.splice(removeIndex, remove); geometry.setCoordinates(coordinates); if (index === 1) { segmentData.index -= remove; } } } } /** * @param {import('./tracing.js').TraceTarget} target The trace target. * @param {number} fromIndex The start index. * @param {number} toIndex The end index. * @private */ addTracedCoordinates_(target, fromIndex, toIndex) { if (fromIndex === toIndex) { return; } const newCoordinates = []; if (fromIndex < toIndex) { // forward trace const start = Math.ceil(fromIndex); let end = Math.floor(toIndex); if (end === toIndex) { // if end is snapped to a vertex, it will be added later end -= 1; } for (let i = start; i <= end; ++i) { newCoordinates.push(getCoordinate(target.coordinates, i)); } } else { // reverse trace const start = Math.floor(fromIndex); let end = Math.ceil(toIndex); if (end === toIndex) { end += 1; } for (let i = start; i >= end; --i) { newCoordinates.push(getCoordinate(target.coordinates, i)); } } if (newCoordinates.length) { for (const traceSegment of this.traceSegments_) { const segmentData = traceSegment[0]; const geometry = segmentData.geometry; const index = traceSegment[1]; const insertIndex = segmentData.index + 1; if (index === 0) { newCoordinates.reverse(); } const coordinates = geometry.getCoordinates(); const coordinatesArray = getCoordinatesArray( coordinates, geometry.getType(), segmentData.depth, ); coordinatesArray.splice(insertIndex, 0, ...newCoordinates); geometry.setCoordinates(coordinates); if (index === 1) { segmentData.index += newCoordinates.length; } } } } /** * @param {import('../coordinate.js').Coordinate} vertex Vertex. * @param {DragSegment} dragSegment Drag segment. */ updateGeometry_(vertex, dragSegment) { const segmentData = dragSegment[0]; const depth = segmentData.depth; let coordinates; const segment = segmentData.segment; const geometry = segmentData.geometry; const index = dragSegment[1]; while (vertex.length < geometry.getStride()) { vertex.push(segment[index][vertex.length]); } switch (geometry.getType()) { case 'Point': coordinates = vertex; segment[0] = vertex; segment[1] = vertex; break; case 'MultiPoint': coordinates = geometry.getCoordinates(); coordinates[segmentData.index] = vertex; segment[0] = vertex; segment[1] = vertex; break; case 'LineString': coordinates = geometry.getCoordinates(); coordinates[segmentData.index + index] = vertex; segment[index] = vertex; break; case 'MultiLineString': coordinates = geometry.getCoordinates(); coordinates[depth[0]][segmentData.index + index] = vertex; segment[index] = vertex; break; case 'Polygon': { coordinates = geometry.getCoordinates(); const ring = coordinates[depth[0]]; const targetIndex = segmentData.index + index; // Prevent duplicate change events when vertex already at position if ( ring[targetIndex][0] === vertex[0] && ring[targetIndex][1] === vertex[1] ) { coordinates = null; } else { ring[targetIndex] = vertex; if (targetIndex === 0) { ring[ring.length - 1] = vertex; } else if (targetIndex === ring.length - 1) { ring[0] = vertex; } } segment[index] = vertex; break; } case 'MultiPolygon': { coordinates = geometry.getCoordinates(); const mRing = coordinates[depth[1]][depth[0]]; const mTargetIndex = segmentData.index + index; // Prevent duplicate change events when vertex already at position if ( mRing[mTargetIndex][0] === vertex[0] && mRing[mTargetIndex][1] === vertex[1] ) { coordinates = null; } else { mRing[mTargetIndex] = vertex; if (mTargetIndex === 0) { mRing[mRing.length - 1] = vertex; } else if (mTargetIndex === mRing.length - 1) { mRing[0] = vertex; } } segment[index] = vertex; break; } case 'Circle': const circle = /** @type {import("../geom/Circle.js").default} */ ( geometry ); segment[0] = vertex; segment[1] = vertex; if (segmentData.index === CIRCLE_CENTER_INDEX) { this.changingFeature_ = true; circle.setCenter(vertex); this.changingFeature_ = false; } else { // We're dragging the circle's circumference: this.changingFeature_ = true; const projection = this.getMap().getView().getProjection(); let radius = coordinateDistance( fromUserCoordinate(circle.getCenter(), projection), fromUserCoordinate(vertex, projection), ); const userProjection = getUserProjection(); if (userProjection) { const circleGeometry = circle .clone() .transform(userProjection, projection); circleGeometry.setRadius(radius); radius = circleGeometry .transform(projection, userProjection) .getRadius(); } circle.setRadius(radius); this.changingFeature_ = false; } break; default: // pass } if (coordinates) { this.setGeometryCoordinates_(geometry, coordinates); } } /** * Handle pointer drag events. * @param {import("../MapBrowserEvent.js").default} evt Event. * @override */ handleDragEvent(evt) { this.ignoreNextSingleClick_ = false; this.willModifyFeatures_( evt, this.dragSegments_.map(([segment]) => segment), ); const vertex = [ evt.coordinate[0] + this.delta_[0], evt.coordinate[1] + this.delta_[1], ]; const features = []; const geometries = []; const startTraceCoord = this.traceState_.active && !this.traceSegments_ ? this.traceState_.startCoord : null; if (startTraceCoord) { this.traceSegments_ = []; for (const dragSegment of this.dragSegments_) { const segmentData = dragSegment[0]; const eligibleForTracing = coordinateDistance( closestOnSegment(startTraceCoord, segmentData.segment), startTraceCoord, ) / evt.map.getView().getResolution() < 1; if (eligibleForTracing) { this.traceSegments_.push(dragSegment); } } } for (let i = 0, ii = this.dragSegments_.length; i < ii; ++i) {