UNPKG

ol

Version:

OpenLayers mapping library

585 lines • 24.1 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); /** * @module ol/interaction/Snap */ import { getUid } from '../util.js'; import CollectionEventType from '../CollectionEventType.js'; import { distance as coordinateDistance, squaredDistance as squaredCoordinateDistance, closestOnCircle, closestOnSegment, squaredDistanceToSegment } from '../coordinate.js'; import { listen, unlistenByKey } from '../events.js'; import EventType from '../events/EventType.js'; import { boundingExtent, createEmpty } from '../extent.js'; import { TRUE, FALSE } from '../functions.js'; import GeometryType from '../geom/GeometryType.js'; import { fromCircle } from '../geom/Polygon.js'; import PointerInteraction from './Pointer.js'; import { getValues } from '../obj.js'; import VectorEventType from '../source/VectorEventType.js'; import RBush from '../structs/RBush.js'; import { getUserProjection, fromUserCoordinate, toUserCoordinate } from '../proj.js'; /** * @typedef {Object} Result * @property {boolean} snapped * @property {import("../coordinate.js").Coordinate|null} vertex * @property {import("../pixel.js").Pixel|null} vertexPixel */ /** * @typedef {Object} SegmentData * @property {import("../Feature.js").default} feature * @property {Array<import("../coordinate.js").Coordinate>} segment */ /** * @typedef {Object} Options * @property {import("../Collection.js").default<import("../Feature.js").default>} [features] Snap to these features. Either this option or source should be provided. * @property {boolean} [edge=true] Snap to edges. * @property {boolean} [vertex=true] Snap to vertices. * @property {number} [pixelTolerance=10] Pixel tolerance for considering the pointer close enough to a segment or * vertex for snapping. * @property {import("../source/Vector.js").default} [source] Snap to features from this source. Either this option or features should be provided */ /** * @param {import("../source/Vector.js").VectorSourceEvent|import("../Collection.js").CollectionEvent} evt Event. * @return {import("../Feature.js").default} Feature. */ function getFeatureFromEvent(evt) { if ( /** @type {import("../source/Vector.js").VectorSourceEvent} */(evt).feature) { return /** @type {import("../source/Vector.js").VectorSourceEvent} */ (evt).feature; } else if ( /** @type {import("../Collection.js").CollectionEvent} */(evt).element) { return /** @type {import("../Feature.js").default} */ ( /** @type {import("../Collection.js").CollectionEvent} */(evt).element); } } var tempSegment = []; /** * @classdesc * Handles snapping of vector features while modifying or drawing them. The * features can come from a {@link module:ol/source/Vector} or {@link module:ol/Collection~Collection} * Any interaction object that allows the user to interact * with the features using the mouse can benefit from the snapping, as long * as it is added before. * * The snap interaction modifies map browser event `coordinate` and `pixel` * properties to force the snap to occur to any interaction that them. * * Example: * * import Snap from 'ol/interaction/Snap'; * * const snap = new Snap({ * source: source * }); * * map.addInteraction(snap); * * @api */ var Snap = /** @class */ (function (_super) { __extends(Snap, _super); /** * @param {Options=} opt_options Options. */ function Snap(opt_options) { var _this = this; var options = opt_options ? opt_options : {}; var pointerOptions = /** @type {import("./Pointer.js").Options} */ (options); if (!pointerOptions.handleDownEvent) { pointerOptions.handleDownEvent = TRUE; } if (!pointerOptions.stopDown) { pointerOptions.stopDown = FALSE; } _this = _super.call(this, pointerOptions) || this; /** * @type {import("../source/Vector.js").default} * @private */ _this.source_ = options.source ? options.source : null; /** * @private * @type {boolean} */ _this.vertex_ = options.vertex !== undefined ? options.vertex : true; /** * @private * @type {boolean} */ _this.edge_ = options.edge !== undefined ? options.edge : true; /** * @type {import("../Collection.js").default<import("../Feature.js").default>} * @private */ _this.features_ = options.features ? options.features : null; /** * @type {Array<import("../events.js").EventsKey>} * @private */ _this.featuresListenerKeys_ = []; /** * @type {Object<string, import("../events.js").EventsKey>} * @private */ _this.featureChangeListenerKeys_ = {}; /** * Extents are preserved so indexed segment can be quickly removed * when its feature geometry changes * @type {Object<string, import("../extent.js").Extent>} * @private */ _this.indexedFeaturesExtents_ = {}; /** * If a feature geometry changes while a pointer drag|move event occurs, the * feature doesn't get updated right away. It will be at the next 'pointerup' * event fired. * @type {!Object<string, import("../Feature.js").default>} * @private */ _this.pendingFeatures_ = {}; /** * @type {number} * @private */ _this.pixelTolerance_ = options.pixelTolerance !== undefined ? options.pixelTolerance : 10; /** * Segment RTree for each layer * @type {import("../structs/RBush.js").default<SegmentData>} * @private */ _this.rBush_ = new RBush(); /** * @const * @private * @type {Object<string, function(import("../Feature.js").default, 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), 'GeometryCollection': _this.writeGeometryCollectionGeometry_.bind(_this), 'Circle': _this.writeCircleGeometry_.bind(_this) }; return _this; } /** * Add a feature to the collection of features that we may snap to. * @param {import("../Feature.js").default} feature Feature. * @param {boolean=} opt_listen Whether to listen to the feature change or not * Defaults to `true`. * @api */ Snap.prototype.addFeature = function (feature, opt_listen) { var register = opt_listen !== undefined ? opt_listen : true; var feature_uid = getUid(feature); var geometry = feature.getGeometry(); if (geometry) { var segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()]; if (segmentWriter) { this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(createEmpty()); segmentWriter(feature, geometry); } } if (register) { this.featureChangeListenerKeys_[feature_uid] = listen(feature, EventType.CHANGE, this.handleFeatureChange_, this); } }; /** * @param {import("../Feature.js").default} feature Feature. * @private */ Snap.prototype.forEachFeatureAdd_ = function (feature) { this.addFeature(feature); }; /** * @param {import("../Feature.js").default} feature Feature. * @private */ Snap.prototype.forEachFeatureRemove_ = function (feature) { this.removeFeature(feature); }; /** * @return {import("../Collection.js").default<import("../Feature.js").default>|Array<import("../Feature.js").default>} Features. * @private */ Snap.prototype.getFeatures_ = function () { var features; if (this.features_) { features = this.features_; } else if (this.source_) { features = this.source_.getFeatures(); } return features; }; /** * @inheritDoc */ Snap.prototype.handleEvent = function (evt) { var result = this.snapTo(evt.pixel, evt.coordinate, evt.map); if (result.snapped) { evt.coordinate = result.vertex.slice(0, 2); evt.pixel = result.vertexPixel; } return _super.prototype.handleEvent.call(this, evt); }; /** * @param {import("../source/Vector.js").VectorSourceEvent|import("../Collection.js").CollectionEvent} evt Event. * @private */ Snap.prototype.handleFeatureAdd_ = function (evt) { var feature = getFeatureFromEvent(evt); this.addFeature(feature); }; /** * @param {import("../source/Vector.js").VectorSourceEvent|import("../Collection.js").CollectionEvent} evt Event. * @private */ Snap.prototype.handleFeatureRemove_ = function (evt) { var feature = getFeatureFromEvent(evt); this.removeFeature(feature); }; /** * @param {import("../events/Event.js").default} evt Event. * @private */ Snap.prototype.handleFeatureChange_ = function (evt) { var feature = /** @type {import("../Feature.js").default} */ (evt.target); if (this.handlingDownUpSequence) { var uid = getUid(feature); if (!(uid in this.pendingFeatures_)) { this.pendingFeatures_[uid] = feature; } } else { this.updateFeature_(feature); } }; /** * @inheritDoc */ Snap.prototype.handleUpEvent = function (evt) { var featuresToUpdate = getValues(this.pendingFeatures_); if (featuresToUpdate.length) { featuresToUpdate.forEach(this.updateFeature_.bind(this)); this.pendingFeatures_ = {}; } return false; }; /** * Remove a feature from the collection of features that we may snap to. * @param {import("../Feature.js").default} feature Feature * @param {boolean=} opt_unlisten Whether to unlisten to the feature change * or not. Defaults to `true`. * @api */ Snap.prototype.removeFeature = function (feature, opt_unlisten) { var unregister = opt_unlisten !== undefined ? opt_unlisten : true; var feature_uid = getUid(feature); var extent = this.indexedFeaturesExtents_[feature_uid]; if (extent) { var rBush = this.rBush_; var nodesToRemove_1 = []; rBush.forEachInExtent(extent, function (node) { if (feature === node.feature) { nodesToRemove_1.push(node); } }); for (var i = nodesToRemove_1.length - 1; i >= 0; --i) { rBush.remove(nodesToRemove_1[i]); } } if (unregister) { unlistenByKey(this.featureChangeListenerKeys_[feature_uid]); delete this.featureChangeListenerKeys_[feature_uid]; } }; /** * @inheritDoc */ Snap.prototype.setMap = function (map) { var currentMap = this.getMap(); var keys = this.featuresListenerKeys_; var features = /** @type {Array<import("../Feature.js").default>} */ (this.getFeatures_()); if (currentMap) { keys.forEach(unlistenByKey); keys.length = 0; features.forEach(this.forEachFeatureRemove_.bind(this)); } _super.prototype.setMap.call(this, map); if (map) { if (this.features_) { keys.push(listen(this.features_, CollectionEventType.ADD, this.handleFeatureAdd_, this), listen(this.features_, CollectionEventType.REMOVE, this.handleFeatureRemove_, this)); } else if (this.source_) { keys.push(listen(this.source_, VectorEventType.ADDFEATURE, this.handleFeatureAdd_, this), listen(this.source_, VectorEventType.REMOVEFEATURE, this.handleFeatureRemove_, this)); } features.forEach(this.forEachFeatureAdd_.bind(this)); } }; /** * @param {import("../pixel.js").Pixel} pixel Pixel * @param {import("../coordinate.js").Coordinate} pixelCoordinate Coordinate * @param {import("../PluggableMap.js").default} map Map. * @return {Result} Snap result */ Snap.prototype.snapTo = function (pixel, pixelCoordinate, map) { var lowerLeft = map.getCoordinateFromPixel([pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]); var upperRight = map.getCoordinateFromPixel([pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]); var box = boundingExtent([lowerLeft, upperRight]); var segments = this.rBush_.getInExtent(box); // If snapping on vertices only, don't consider circles if (this.vertex_ && !this.edge_) { segments = segments.filter(function (segment) { return segment.feature.getGeometry().getType() !== GeometryType.CIRCLE; }); } var snapped = false; var vertex = null; var vertexPixel = null; if (segments.length === 0) { return { snapped: snapped, vertex: vertex, vertexPixel: vertexPixel }; } var projection = map.getView().getProjection(); var projectedCoordinate = fromUserCoordinate(pixelCoordinate, projection); var closestSegmentData; var minSquaredDistance = Infinity; for (var i = 0; i < segments.length; ++i) { var segmentData = segments[i]; tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection); tempSegment[1] = fromUserCoordinate(segmentData.segment[1], projection); var delta = squaredDistanceToSegment(projectedCoordinate, tempSegment); if (delta < minSquaredDistance) { closestSegmentData = segmentData; minSquaredDistance = delta; } } var closestSegment = closestSegmentData.segment; if (this.vertex_ && !this.edge_) { var pixel1 = map.getPixelFromCoordinate(closestSegment[0]); var pixel2 = map.getPixelFromCoordinate(closestSegment[1]); var squaredDist1 = squaredCoordinateDistance(pixel, pixel1); var squaredDist2 = squaredCoordinateDistance(pixel, pixel2); var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); if (dist <= this.pixelTolerance_) { snapped = true; vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; vertexPixel = map.getPixelFromCoordinate(vertex); } } else if (this.edge_) { var isCircle = closestSegmentData.feature.getGeometry().getType() === GeometryType.CIRCLE; if (isCircle) { var circleGeometry = closestSegmentData.feature.getGeometry(); var userProjection = getUserProjection(); if (userProjection) { circleGeometry = circleGeometry.clone().transform(userProjection, projection); } vertex = toUserCoordinate(closestOnCircle(projectedCoordinate, /** @type {import("../geom/Circle.js").default} */ (circleGeometry)), projection); } else { tempSegment[0] = fromUserCoordinate(closestSegment[0], projection); tempSegment[1] = fromUserCoordinate(closestSegment[1], projection); vertex = toUserCoordinate(closestOnSegment(projectedCoordinate, tempSegment), projection); } vertexPixel = map.getPixelFromCoordinate(vertex); if (coordinateDistance(pixel, vertexPixel) <= this.pixelTolerance_) { snapped = true; if (this.vertex_ && !isCircle) { var pixel1 = map.getPixelFromCoordinate(closestSegment[0]); var pixel2 = map.getPixelFromCoordinate(closestSegment[1]); var squaredDist1 = squaredCoordinateDistance(vertexPixel, pixel1); var squaredDist2 = squaredCoordinateDistance(vertexPixel, pixel2); var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2)); if (dist <= this.pixelTolerance_) { vertex = squaredDist1 > squaredDist2 ? closestSegment[1] : closestSegment[0]; vertexPixel = map.getPixelFromCoordinate(vertex); } } } } if (snapped) { vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])]; } return { snapped: snapped, vertex: vertex, vertexPixel: vertexPixel }; }; /** * @param {import("../Feature.js").default} feature Feature * @private */ Snap.prototype.updateFeature_ = function (feature) { this.removeFeature(feature, false); this.addFeature(feature, false); }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/Circle.js").default} geometry Geometry. * @private */ Snap.prototype.writeCircleGeometry_ = function (feature, geometry) { var projection = this.getMap().getView().getProjection(); var circleGeometry = geometry; var userProjection = getUserProjection(); if (userProjection) { circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection)); } var polygon = fromCircle(circleGeometry); if (userProjection) { polygon.transform(projection, userProjection); } var coordinates = polygon.getCoordinates()[0]; for (var i = 0, ii = coordinates.length - 1; i < ii; ++i) { var segment = coordinates.slice(i, i + 2); var segmentData = { feature: feature, segment: segment }; this.rBush_.insert(boundingExtent(segment), segmentData); } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @private */ Snap.prototype.writeGeometryCollectionGeometry_ = function (feature, geometry) { var geometries = geometry.getGeometriesArray(); for (var i = 0; i < geometries.length; ++i) { var segmentWriter = this.SEGMENT_WRITERS_[geometries[i].getType()]; if (segmentWriter) { segmentWriter(feature, geometries[i]); } } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/LineString.js").default} geometry Geometry. * @private */ Snap.prototype.writeLineStringGeometry_ = function (feature, geometry) { var coordinates = geometry.getCoordinates(); for (var i = 0, ii = coordinates.length - 1; i < ii; ++i) { var segment = coordinates.slice(i, i + 2); var segmentData = { feature: feature, segment: segment }; this.rBush_.insert(boundingExtent(segment), segmentData); } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/MultiLineString.js").default} geometry Geometry. * @private */ Snap.prototype.writeMultiLineStringGeometry_ = function (feature, geometry) { var lines = geometry.getCoordinates(); for (var j = 0, jj = lines.length; j < jj; ++j) { var coordinates = lines[j]; for (var i = 0, ii = coordinates.length - 1; i < ii; ++i) { var segment = coordinates.slice(i, i + 2); var segmentData = { feature: feature, segment: segment }; this.rBush_.insert(boundingExtent(segment), segmentData); } } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/MultiPoint.js").default} geometry Geometry. * @private */ Snap.prototype.writeMultiPointGeometry_ = function (feature, geometry) { var points = geometry.getCoordinates(); for (var i = 0, ii = points.length; i < ii; ++i) { var coordinates = points[i]; var segmentData = { feature: feature, segment: [coordinates, coordinates] }; this.rBush_.insert(geometry.getExtent(), segmentData); } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @private */ Snap.prototype.writeMultiPolygonGeometry_ = function (feature, geometry) { var polygons = geometry.getCoordinates(); for (var k = 0, kk = polygons.length; k < kk; ++k) { var rings = polygons[k]; for (var j = 0, jj = rings.length; j < jj; ++j) { var coordinates = rings[j]; for (var i = 0, ii = coordinates.length - 1; i < ii; ++i) { var segment = coordinates.slice(i, i + 2); var segmentData = { feature: feature, segment: segment }; this.rBush_.insert(boundingExtent(segment), segmentData); } } } }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/Point.js").default} geometry Geometry. * @private */ Snap.prototype.writePointGeometry_ = function (feature, geometry) { var coordinates = geometry.getCoordinates(); var segmentData = { feature: feature, segment: [coordinates, coordinates] }; this.rBush_.insert(geometry.getExtent(), segmentData); }; /** * @param {import("../Feature.js").default} feature Feature * @param {import("../geom/Polygon.js").default} geometry Geometry. * @private */ Snap.prototype.writePolygonGeometry_ = function (feature, geometry) { var rings = geometry.getCoordinates(); for (var j = 0, jj = rings.length; j < jj; ++j) { var coordinates = rings[j]; for (var i = 0, ii = coordinates.length - 1; i < ii; ++i) { var segment = coordinates.slice(i, i + 2); var segmentData = { feature: feature, segment: segment }; this.rBush_.insert(boundingExtent(segment), segmentData); } } }; return Snap; }(PointerInteraction)); export default Snap; //# sourceMappingURL=Snap.js.map