UNPKG

ol-rotate-feature

Version:

Rotate vector features interaction for OpenLayers

835 lines (778 loc) 23 kB
/*! Rotate vector features interaction for OpenLayers @package ol-rotate-feature @author Vladimir Vershinin <ghettovoice@gmail.com> @version 3.3.0 @licence MIT @copyright (c) 2016-2025, Vladimir Vershinin <ghettovoice@gmail.com> */ import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _typeof from '@babel/runtime/helpers/typeof'; import _classCallCheck from '@babel/runtime/helpers/classCallCheck'; import _createClass from '@babel/runtime/helpers/createClass'; import _possibleConstructorReturn from '@babel/runtime/helpers/possibleConstructorReturn'; import _get from '@babel/runtime/helpers/get'; import _getPrototypeOf from '@babel/runtime/helpers/getPrototypeOf'; import _inherits from '@babel/runtime/helpers/inherits'; import { Pointer } from 'ol/interaction'; import { Feature, Collection } from 'ol'; import { Vector } from 'ol/layer'; import VectorSource from 'ol/source/Vector'; import { Point, Polygon, GeometryCollection } from 'ol/geom'; import { Style, RegularShape, Fill, Stroke, Text } from 'ol/style'; import { getCenter } from 'ol/extent'; import { always, mouseOnly, touchOnly, penOnly } from 'ol/events/condition'; /** * This file is part of ol-rotate-feature package. * @module ol-rotate-feature * @license MIT * @author Vladimir Vershinin */ /** * @param {boolean} condition * @param {string} message * @throws Error */ function assert(condition) { var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; message = ['Assertion failed', message].join(': '); if (!condition) { throw new Error(message); } } /** * @param {*} arg * @returns {*} */ function identity(arg) { return arg; } function includes(arr, value) { return arr.indexOf(value) !== -1; } function isArray(val) { return Object.prototype.toString.call(val) === '[object Array]'; } /** * This file is part of ol-rotate-feature package. * @module ol-rotate-feature * @license MIT * @author Vladimir Vershinin */ /** * @enum {string} */ var RotateFeatureEventType = { /** * Triggered upon feature rotate start. * @event RotateFeatureEvent#rotatestart */ START: 'rotatestart', /** * Triggered upon feature rotation. * @event RotateFeatureEvent#rotating */ ROTATING: 'rotating', /** * Triggered upon feature rotation end. * @event RotateFeatureEvent#rotateend */ END: 'rotateend' }; /** * Events emitted by RotateFeatureInteraction instances are instances of this type. * * @class * @author Vladimir Vershinin */ var RotateFeatureEvent = /*#__PURE__*/function () { /** * @param {string} type Type. * @param {ol.Collection<ol.Feature>} features Rotated features. * @param {number} angle Angle in radians. * @param {ol.Coordinate} anchor Anchor position. */ function RotateFeatureEvent(type, features, angle, anchor) { _classCallCheck(this, RotateFeatureEvent); /** * @type {boolean} * @private */ this.propagationStopped_ = false; /** * The event type. * @type {string} * @private */ this.type_ = type; /** * The features being rotated. * @type {ol.Collection<ol.Feature>} * @private */ this.features_ = features; /** * Current angle in radians. * @type {number} * @private */ this.angle_ = angle; /** * Current rotation anchor. * @type {ol.Coordinate} * @private */ this.anchor_ = anchor; } /** * @type {boolean} */ return _createClass(RotateFeatureEvent, [{ key: "propagationStopped", get: function get() { return this.propagationStopped_; } /** * @type {string} */ }, { key: "type", get: function get() { return this.type_; } /** * @type {ol.Collection<ol.Feature>} */ }, { key: "features", get: function get() { return this.features_; } /** * @type {number} */ }, { key: "angle", get: function get() { return this.angle_; } /** * @type {ol.Coordinate} */ }, { key: "anchor", get: function get() { return this.anchor_; } /** * Prevent event propagation. */ }, { key: "preventDefault", value: function preventDefault() { this.propagationStopped_ = true; } /** * Stop event propagation. */ }, { key: "stopPropagation", value: function stopPropagation() { this.propagationStopped_ = true; } }]); }(); var ua = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''; var MAC = ua.indexOf('macintosh') !== -1; var WEBKIT = ua.indexOf('webkit') !== -1 && ua.indexOf('edge') == -1; var mouseActionButton = function mouseActionButton(mapBrowserEvent) { var originalEvent = /** @type {MouseEvent} */mapBrowserEvent.originalEvent; return originalEvent.button == 0 && !(WEBKIT && MAC && originalEvent.ctrlKey); }; function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); } function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); } var ANCHOR_KEY = 'rotate-anchor'; var ARROW_KEY = 'rotate-arrow'; var ANGLE_PROP = 'angle'; var ANCHOR_PROP = 'anchor'; /** * @todo todo добавить опцию condition - для возможности переопределения клавиш */ var RotateFeatureInteraction = /*#__PURE__*/function (_PointerInteraction) { /** * @param {InteractionOptions} options */ function RotateFeatureInteraction() { var _this; var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, RotateFeatureInteraction); _this = _callSuper(this, RotateFeatureInteraction, [{ // handleEvent: handleEvent, handleDownEvent: handleDownEvent, handleUpEvent: handleUpEvent, handleDragEvent: handleDragEvent, handleMoveEvent: handleMoveEvent }]); /** * @type {string} * @private */ _this.previousCursor_ = undefined; /** * @type {Feature} * @private */ _this.anchorFeature_ = undefined; /** * @type {Feature} * @private */ _this.arrowFeature_ = undefined; /** * @type {Coordinate} * @private */ _this.lastCoordinate_ = undefined; /** * @type {boolean} * @private */ _this.anchorMoving_ = false; /** * @type {Vector} * @private */ _this.overlay_ = new Vector({ style: options.style || getDefaultStyle(), source: new VectorSource({ features: new Collection() }) }); /** * @private * @type {module:ol/events/condition~Condition} */ _this.condition_ = options.condition ? options.condition : always; /** * @type {Collection<Feature>} * @private */ _this.features_ = undefined; if (options.features) { if (isArray(options.features)) { _this.features_ = new Collection(options.features); } else if (options.features instanceof Collection) { _this.features_ = options.features; } else { throw new Error('Features option should be an array or collection of features, ' + 'got ' + _typeof(options.features)); } } else { _this.features_ = new Collection(); } /** * @type {boolean} * @public */ _this.allowAnchorMovement = options.allowAnchorMovement === undefined ? true : options.allowAnchorMovement; _this.setAnchor(options.anchor || getFeaturesCentroid(_this.features_)); _this.setAngle(options.angle || 0); _this.features_.on('add', _this.onFeatureAdd_.bind(_this)); _this.features_.on('remove', _this.onFeatureRemove_.bind(_this)); _this.on('change:' + ANGLE_PROP, _this.onAngleChange_.bind(_this)); _this.on('change:' + ANCHOR_PROP, _this.onAnchorChange_.bind(_this)); _this.createOrUpdateAnchorFeature_(); _this.createOrUpdateArrowFeature_(); return _this; } /** * @type {Collection<Feature>} */ _inherits(RotateFeatureInteraction, _PointerInteraction); return _createClass(RotateFeatureInteraction, [{ key: "features", get: function get() { return this.features_; } /** * @type {number} */ }, { key: "angle", get: function get() { return this.getAngle(); } /** * @param {number} angle */, set: function set(angle) { this.setAngle(angle); } /** * @type {Coordinate|number[]|undefined} */ }, { key: "anchor", get: function get() { return this.getAnchor(); } /** * @param {Coordinate|undefined} anchor */, set: function set(anchor) { this.setAnchor(anchor); } /** * @param {PluggableMap} map */ }, { key: "map", get: /** * @type {PluggableMap} */ function get() { return this.getMap(); } /** * @param {boolean} active */, set: function set(map) { this.setMap(map); } }, { key: "active", get: /** * @type {boolean} */ function get() { return this.getActive(); } /** * @param {ol.Map} map */, set: function set(active) { this.setActive(active); } }, { key: "setMap", value: function setMap(map) { this.overlay_.setMap(map); _get(_getPrototypeOf(RotateFeatureInteraction.prototype), "setMap", this).call(this, map); } /** * @param {boolean} active */ }, { key: "setActive", value: function setActive(active) { if (this.overlay_) { this.overlay_.setMap(active ? this.map : undefined); } _get(_getPrototypeOf(RotateFeatureInteraction.prototype), "setActive", this).call(this, active); } /** * Set current angle of interaction features. * * @param {number} angle */ }, { key: "setAngle", value: function setAngle(angle) { assert(!isNaN(parseFloat(angle)), 'Numeric value passed'); this.set(ANGLE_PROP, parseFloat(angle)); } /** * Returns current angle of interaction features. * * @return {number} */ }, { key: "getAngle", value: function getAngle() { return this.get(ANGLE_PROP); } /** * Set current anchor position. * * @param {Coordinate | undefined} anchor */ }, { key: "setAnchor", value: function setAnchor(anchor) { assert(anchor == null || isArray(anchor) && anchor.length === 2, 'Array of two elements passed'); this.set(ANCHOR_PROP, anchor != null ? anchor.map(parseFloat) : getFeaturesCentroid(this.features_)); } /** * Returns current anchor position. * * @return {Coordinate | undefined} */ }, { key: "getAnchor", value: function getAnchor() { return this.get(ANCHOR_PROP); } /** * @private */ }, { key: "createOrUpdateAnchorFeature_", value: function createOrUpdateAnchorFeature_() { var angle = this.getAngle(); var anchor = this.getAnchor(); if (!anchor) return; if (this.anchorFeature_) { this.anchorFeature_.getGeometry().setCoordinates(anchor); this.anchorFeature_.set(ANGLE_PROP, angle); } else { this.anchorFeature_ = new Feature(_defineProperty(_defineProperty({ geometry: new Point(anchor) }, ANGLE_PROP, angle), ANCHOR_KEY, true)); this.overlay_.getSource().addFeature(this.anchorFeature_); } } /** * @private */ }, { key: "createOrUpdateArrowFeature_", value: function createOrUpdateArrowFeature_() { var angle = this.getAngle(); var anchor = this.getAnchor(); if (!anchor) return; if (this.arrowFeature_) { this.arrowFeature_.getGeometry().setCoordinates(anchor); this.arrowFeature_.set(ANGLE_PROP, angle); } else { this.arrowFeature_ = new Feature(_defineProperty(_defineProperty({ geometry: new Point(anchor) }, ANGLE_PROP, angle), ARROW_KEY, true)); this.overlay_.getSource().addFeature(this.arrowFeature_); } } /** * @private */ }, { key: "resetAngleAndAnchor_", value: function resetAngleAndAnchor_() { this.resetAngle_(); this.resetAnchor_(); } /** * @private */ }, { key: "resetAngle_", value: function resetAngle_() { this.set(ANGLE_PROP, 0, true); this.arrowFeature_ && this.arrowFeature_.set(ANGLE_PROP, this.getAngle()); this.anchorFeature_ && this.anchorFeature_.set(ANGLE_PROP, this.getAngle()); } /** * @private */ }, { key: "resetAnchor_", value: function resetAnchor_() { this.set(ANCHOR_PROP, getFeaturesCentroid(this.features_), true); if (this.getAnchor()) { this.arrowFeature_ && this.arrowFeature_.getGeometry().setCoordinates(this.getAnchor()); this.anchorFeature_ && this.anchorFeature_.getGeometry().setCoordinates(this.getAnchor()); } } /** * @private */ }, { key: "onFeatureAdd_", value: function onFeatureAdd_() { this.resetAngleAndAnchor_(); this.createOrUpdateAnchorFeature_(); this.createOrUpdateArrowFeature_(); } /** * @private */ }, { key: "onFeatureRemove_", value: function onFeatureRemove_() { this.resetAngleAndAnchor_(); if (this.features_.getLength()) { this.createOrUpdateAnchorFeature_(); this.createOrUpdateArrowFeature_(); } else { this.overlay_.getSource().clear(); this.anchorFeature_ = this.arrowFeature_ = undefined; } } /** * @private */ }, { key: "onAngleChange_", value: function onAngleChange_(_ref) { var _this2 = this; var oldValue = _ref.oldValue; this.features_.forEach(function (feature) { return feature.getGeometry().rotate(_this2.getAngle() - oldValue, _this2.getAnchor()); }); this.arrowFeature_ && this.arrowFeature_.set(ANGLE_PROP, this.getAngle()); this.anchorFeature_ && this.anchorFeature_.set(ANGLE_PROP, this.getAngle()); } /** * @private */ }, { key: "onAnchorChange_", value: function onAnchorChange_() { var anchor = this.getAnchor(); if (anchor) { this.anchorFeature_ && this.anchorFeature_.getGeometry().setCoordinates(anchor); this.arrowFeature_ && this.arrowFeature_.getGeometry().setCoordinates(anchor); } } /** * @param {Collection<Feature>} features * @private */ }, { key: "dispatchRotateStartEvent_", value: function dispatchRotateStartEvent_(features) { this.dispatchEvent(new RotateFeatureEvent(RotateFeatureEventType.START, features, this.getAngle(), this.getAnchor())); } /** * @param {Collection<Feature>} features * @private */ }, { key: "dispatchRotatingEvent_", value: function dispatchRotatingEvent_(features) { this.dispatchEvent(new RotateFeatureEvent(RotateFeatureEventType.ROTATING, features, this.getAngle(), this.getAnchor())); } /** * @param {Collection<Feature>} features * @private */ }, { key: "dispatchRotateEndEvent_", value: function dispatchRotateEndEvent_(features) { this.dispatchEvent(new RotateFeatureEvent(RotateFeatureEventType.END, features, this.getAngle(), this.getAnchor())); } }]); }(Pointer); function handleDownEvent(evt) { if (!(mouseOnly(evt) || touchOnly(evt) || penOnly(evt))) { return false; } if (mouseActionButton(evt) && this.condition_(evt)) { // disable selection of inner features var foundFeature = evt.map.forEachFeatureAtPixel(evt.pixel, identity); if (includes(['click', 'singleclick', 'dblclick'], evt.type) && includes([this.anchorFeature_, this.arrowFeature_], foundFeature)) { return false; } // handle click & drag on features for rotation if (foundFeature && !this.lastCoordinate_ && (includes(this.features_.getArray(), foundFeature) || foundFeature === this.arrowFeature_)) { this.lastCoordinate_ = evt.coordinate; handleMoveEvent.call(this, evt); this.dispatchRotateStartEvent_(this.features_); return true; } // handle click & drag on rotation anchor feature else if (foundFeature && foundFeature === this.anchorFeature_ && this.allowAnchorMovement) { this.anchorMoving_ = true; handleMoveEvent.call(this, evt); return true; } } return false; } /** * @param {MapBrowserEvent} evt Event. * @return {boolean} * @this {RotateFeatureInteraction} * @private */ function handleUpEvent(evt) { // stop drag sequence of features if (this.lastCoordinate_) { this.lastCoordinate_ = undefined; handleMoveEvent.call(this, evt); this.dispatchRotateEndEvent_(this.features_); return true; } // stop drag sequence of the anchors else if (this.anchorMoving_) { this.anchorMoving_ = false; handleMoveEvent.call(this, evt); return true; } return false; } /** * @param {MapBrowserEvent} evt Event. * @return {boolean} * @this {RotateFeatureInteraction} * @private */ function handleDragEvent(_ref2) { var coordinate = _ref2.coordinate; var anchorCoordinate = this.anchorFeature_.getGeometry().getCoordinates(); // handle drag of features by angle if (this.lastCoordinate_) { // calculate vectors of last and current pointer positions var lastVector = [this.lastCoordinate_[0] - anchorCoordinate[0], this.lastCoordinate_[1] - anchorCoordinate[1]]; var newVector = [coordinate[0] - anchorCoordinate[0], coordinate[1] - anchorCoordinate[1]]; // calculate angle between last and current vectors (positive angle counter-clockwise) var angle = Math.atan2(lastVector[0] * newVector[1] - newVector[0] * lastVector[1], lastVector[0] * newVector[0] + lastVector[1] * newVector[1]); this.setAngle(this.getAngle() + angle); this.dispatchRotatingEvent_(this.features_); this.lastCoordinate_ = coordinate; } // handle drag of the anchor else if (this.anchorMoving_) { this.setAnchor(coordinate); } } /** * @param {MapBrowserEvent} evt Event. * @return {boolean} * @this {RotateFeatureInteraction} * @private */ function handleMoveEvent(_ref3) { var map = _ref3.map, pixel = _ref3.pixel; var elem = map.getTargetElement(); var foundFeature = map.forEachFeatureAtPixel(pixel, identity); var setCursor = function setCursor(cursor) { var vendor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; if (vendor) { elem.style.cursor = '-webkit-' + cursor; elem.style.cursor = '-moz-' + cursor; } elem.style.cursor = cursor; }; if (this.lastCoordinate_) { this.previousCursor_ = elem.style.cursor; setCursor('grabbing', true); } else if (foundFeature && (includes(this.features_.getArray(), foundFeature) || foundFeature === this.arrowFeature_)) { this.previousCursor_ = elem.style.cursor; setCursor('grab', true); } else if (foundFeature && foundFeature === this.anchorFeature_ && this.allowAnchorMovement || this.anchorMoving_) { this.previousCursor_ = elem.style.cursor; setCursor('crosshair'); } else { setCursor(this.previousCursor_ || ''); this.previousCursor_ = undefined; } } /** * @returns {StyleFunction} * @private */ function getDefaultStyle() { var white = [255, 255, 255, 0.8]; var blue = [0, 153, 255, 0.8]; var transparent = [255, 255, 255, 0.01]; var width = 2; var styles = _defineProperty(_defineProperty({}, ANCHOR_KEY, [new Style({ image: new RegularShape({ fill: new Fill({ color: [0, 153, 255, 0.8] }), stroke: new Stroke({ color: blue, width: 1 }), radius: 4, points: 6 }), zIndex: Infinity })]), ARROW_KEY, [new Style({ fill: new Fill({ color: transparent }), stroke: new Stroke({ color: white, width: width + 2 }), text: new Text({ font: '12px sans-serif', offsetX: 20, offsetY: -20, fill: new Fill({ color: 'blue' }), stroke: new Stroke({ color: white, width: width + 1 }) }), zIndex: Infinity }), new Style({ fill: new Fill({ color: transparent }), stroke: new Stroke({ color: blue, width: width }), zIndex: Infinity })]); return function (feature, resolution) { var style; var angle = feature.get(ANGLE_PROP) || 0; switch (true) { case feature.get(ANCHOR_KEY): style = styles[ANCHOR_KEY]; style[0].getImage().setRotation(-angle); return style; case feature.get(ARROW_KEY): style = styles[ARROW_KEY]; var coordinates = feature.getGeometry().getCoordinates(); // generate arrow polygon var geom = new Polygon([[[coordinates[0], coordinates[1] - 6 * resolution], [coordinates[0] + 8 * resolution, coordinates[1] - 12 * resolution], [coordinates[0], coordinates[1] + 30 * resolution], [coordinates[0] - 8 * resolution, coordinates[1] - 12 * resolution], [coordinates[0], coordinates[1] - 6 * resolution]]]); // and rotate it according to current angle geom.rotate(angle, coordinates); style[0].setGeometry(geom); style[1].setGeometry(geom); style[0].getText().setText(Math.round(-angle * 180 / Math.PI) + '°'); return style; } }; } /** * @param {Collection<Feature>|Array<Feature>} features * @returns {Extent | number[] | undefined} * @private */ function getFeaturesExtent(features) { features = features instanceof Collection ? features.getArray() : features; if (!features.length) return; return new GeometryCollection(features.map(function (feature) { return feature.getGeometry(); })).getExtent(); } /** * @param {Collection<ol.Feature> | Array<Feature>} features * @return {Coordinate | number[] | undefined} */ function getFeaturesCentroid(features) { features = features instanceof Collection ? features.getArray() : features; if (!features.length) return; return getCenter(getFeaturesExtent(features)); } /** * This file is part of ol-rotate-feature package. * @module ol-rotate-feature * @license MIT * @author Vladimir Vershinin */ // for backward compatibility if (typeof window !== 'undefined' && window.ol && window.ol.interaction) { window.ol.interaction.RotateFeature = RotateFeatureInteraction; } export { RotateFeatureInteraction as default }; //# sourceMappingURL=ol-rotate-feature.esm.js.map