ol-rotate-feature
Version:
Rotate vector features interaction for OpenLayers
835 lines (778 loc) • 23 kB
JavaScript
/*!
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