package.interaction.Translate.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ol Show documentation
Show all versions of ol Show documentation
OpenLayers mapping library
The newest version!
/**
* @module ol/interaction/Translate
*/
import Collection from '../Collection.js';
import Event from '../events/Event.js';
import Feature from '../Feature.js';
import InteractionProperty from './Property.js';
import PointerInteraction from './Pointer.js';
import {TRUE} from '../functions.js';
import {always} from '../events/condition.js';
import {fromUserCoordinate, getUserProjection} from '../proj.js';
/**
* @enum {string}
*/
const TranslateEventType = {
/**
* Triggered upon feature translation start.
* @event TranslateEvent#translatestart
* @api
*/
TRANSLATESTART: 'translatestart',
/**
* Triggered upon feature translation.
* @event TranslateEvent#translating
* @api
*/
TRANSLATING: 'translating',
/**
* Triggered upon feature translation end.
* @event TranslateEvent#translateend
* @api
*/
TRANSLATEEND: 'translateend',
};
/**
* A function that takes a {@link module:ol/Feature~Feature} or
* {@link module:ol/render/Feature~RenderFeature} and a
* {@link module:ol/layer/Layer~Layer} and returns `true` if the feature may be
* translated or `false` otherwise.
* @typedef {function(Feature, import("../layer/Layer.js").default):boolean} FilterFunction
*/
/**
* @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 should be handled.
* Default is {@link module:ol/events/condition.always}.
* @property {Collection} [features] Features contained in this collection will be able to be translated together.
* @property {Array|function(import("../layer/Layer.js").default): boolean} [layers] A list of layers from which features should be
* translated. Alternatively, a filter function can be provided. The
* function will be called for each layer in the map and should return
* `true` for layers that you want to be translatable. If the option is
* absent, all visible layers will be considered translatable.
* Not used if `features` is provided.
* @property {FilterFunction} [filter] A function
* that takes a {@link module:ol/Feature~Feature} and an
* {@link module:ol/layer/Layer~Layer} and returns `true` if the feature may be
* translated or `false` otherwise. Not used if `features` is provided.
* @property {number} [hitTolerance=0] Hit-detection tolerance. Pixels inside the radius around the given position
* will be checked for features.
*/
/**
* @classdesc
* Events emitted by {@link module:ol/interaction/Translate~Translate} instances
* are instances of this type.
*/
export class TranslateEvent extends Event {
/**
* @param {TranslateEventType} type Type.
* @param {Collection} features The features translated.
* @param {import("../coordinate.js").Coordinate} coordinate The event coordinate.
* @param {import("../coordinate.js").Coordinate} startCoordinate The original coordinates before.translation started
* @param {import("../MapBrowserEvent.js").default} mapBrowserEvent Map browser event.
*/
constructor(type, features, coordinate, startCoordinate, mapBrowserEvent) {
super(type);
/**
* The features being translated.
* @type {Collection}
* @api
*/
this.features = features;
/**
* The coordinate of the drag event.
* @const
* @type {import("../coordinate.js").Coordinate}
* @api
*/
this.coordinate = coordinate;
/**
* The coordinate of the start position before translation started.
* @const
* @type {import("../coordinate.js").Coordinate}
* @api
*/
this.startCoordinate = startCoordinate;
/**
* Associated {@link module:ol/MapBrowserEvent~MapBrowserEvent}.
* @type {import("../MapBrowserEvent.js").default}
* @api
*/
this.mapBrowserEvent = mapBrowserEvent;
}
}
/***
* @template Return
* @typedef {import("../Observable").OnSignature &
* import("../Observable").OnSignature &
* import("../Observable").OnSignature<'translateend'|'translatestart'|'translating', TranslateEvent, Return> &
* import("../Observable").CombinedOnSignature} TranslateOnSignature
*/
/**
* @classdesc
* Interaction for translating (moving) features.
* If you want to translate multiple features in a single action (for example,
* the collection used by a select interaction), construct the interaction with
* the `features` option.
*
* @fires TranslateEvent
* @api
*/
class Translate extends PointerInteraction {
/**
* @param {Options} [options] Options.
*/
constructor(options) {
options = options ? options : {};
super(/** @type {import("./Pointer.js").Options} */ (options));
/***
* @type {TranslateOnSignature}
*/
this.on;
/***
* @type {TranslateOnSignature}
*/
this.once;
/***
* @type {TranslateOnSignature}
*/
this.un;
/**
* The last position we translated to.
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.lastCoordinate_ = null;
/**
* The start position before translation started.
* @type {import("../coordinate.js").Coordinate}
* @private
*/
this.startCoordinate_ = null;
/**
* @type {Collection|null}
* @private
*/
this.features_ = options.features !== undefined ? options.features : null;
/** @type {function(import("../layer/Layer.js").default): boolean} */
let layerFilter;
if (options.layers && !this.features_) {
if (typeof options.layers === 'function') {
layerFilter = options.layers;
} else {
const layers = options.layers;
layerFilter = function (layer) {
return layers.includes(layer);
};
}
} else {
layerFilter = TRUE;
}
/**
* @private
* @type {function(import("../layer/Layer.js").default): boolean}
*/
this.layerFilter_ = layerFilter;
/**
* @private
* @type {FilterFunction}
*/
this.filter_ = options.filter && !this.features_ ? options.filter : TRUE;
/**
* @private
* @type {number}
*/
this.hitTolerance_ = options.hitTolerance ? options.hitTolerance : 0;
/**
* @private
* @type {import("../events/condition.js").Condition}
*/
this.condition_ = options.condition ? options.condition : always;
/**
* @type {Feature}
* @private
*/
this.lastFeature_ = null;
this.addChangeListener(
InteractionProperty.ACTIVE,
this.handleActiveChanged_,
);
}
/**
* Handle pointer down events.
* @param {import("../MapBrowserEvent.js").default} event Event.
* @return {boolean} If the event was consumed.
* @override
*/
handleDownEvent(event) {
if (!event.originalEvent || !this.condition_(event)) {
return false;
}
this.lastFeature_ = this.featuresAtPixel_(event.pixel, event.map);
if (!this.lastCoordinate_ && this.lastFeature_) {
this.startCoordinate_ = event.coordinate;
this.lastCoordinate_ = event.coordinate;
this.handleMoveEvent(event);
const features = this.features_ || new Collection([this.lastFeature_]);
this.dispatchEvent(
new TranslateEvent(
TranslateEventType.TRANSLATESTART,
features,
event.coordinate,
this.startCoordinate_,
event,
),
);
return true;
}
return false;
}
/**
* Handle pointer up events.
* @param {import("../MapBrowserEvent.js").default} event Event.
* @return {boolean} If the event was consumed.
* @override
*/
handleUpEvent(event) {
if (this.lastCoordinate_) {
this.lastCoordinate_ = null;
this.handleMoveEvent(event);
const features = this.features_ || new Collection([this.lastFeature_]);
this.dispatchEvent(
new TranslateEvent(
TranslateEventType.TRANSLATEEND,
features,
event.coordinate,
this.startCoordinate_,
event,
),
);
// cleanup
this.startCoordinate_ = null;
return true;
}
return false;
}
/**
* Handle pointer drag events.
* @param {import("../MapBrowserEvent.js").default} event Event.
* @override
*/
handleDragEvent(event) {
if (this.lastCoordinate_) {
const newCoordinate = event.coordinate;
const projection = event.map.getView().getProjection();
const newViewCoordinate = fromUserCoordinate(newCoordinate, projection);
const lastViewCoordinate = fromUserCoordinate(
this.lastCoordinate_,
projection,
);
const deltaX = newViewCoordinate[0] - lastViewCoordinate[0];
const deltaY = newViewCoordinate[1] - lastViewCoordinate[1];
const features = this.features_ || new Collection([this.lastFeature_]);
const userProjection = getUserProjection();
features.forEach(function (feature) {
const geom = feature.getGeometry();
if (userProjection) {
geom.transform(userProjection, projection);
geom.translate(deltaX, deltaY);
geom.transform(projection, userProjection);
} else {
geom.translate(deltaX, deltaY);
}
feature.setGeometry(geom);
});
this.lastCoordinate_ = newCoordinate;
this.dispatchEvent(
new TranslateEvent(
TranslateEventType.TRANSLATING,
features,
newCoordinate,
this.startCoordinate_,
event,
),
);
}
}
/**
* Handle pointer move events.
* @param {import("../MapBrowserEvent.js").default} event Event.
* @override
*/
handleMoveEvent(event) {
const elem = event.map.getViewport();
// Change the cursor to grab/grabbing if hovering any of the features managed
// by the interaction
if (this.featuresAtPixel_(event.pixel, event.map)) {
elem.classList.remove(this.lastCoordinate_ ? 'ol-grab' : 'ol-grabbing');
elem.classList.add(this.lastCoordinate_ ? 'ol-grabbing' : 'ol-grab');
} else {
elem.classList.remove('ol-grab', 'ol-grabbing');
}
}
/**
* Tests to see if the given coordinates intersects any of our selected
* features.
* @param {import("../pixel.js").Pixel} pixel Pixel coordinate to test for intersection.
* @param {import("../Map.js").default} map Map to test the intersection on.
* @return {Feature} Returns the feature found at the specified pixel
* coordinates.
* @private
*/
featuresAtPixel_(pixel, map) {
return map.forEachFeatureAtPixel(
pixel,
(feature, layer) => {
if (!(feature instanceof Feature) || !this.filter_(feature, layer)) {
return undefined;
}
if (this.features_ && !this.features_.getArray().includes(feature)) {
return undefined;
}
return feature;
},
{
layerFilter: this.layerFilter_,
hitTolerance: this.hitTolerance_,
},
);
}
/**
* Returns the Hit-detection tolerance.
* @return {number} Hit tolerance in pixels.
* @api
*/
getHitTolerance() {
return this.hitTolerance_;
}
/**
* Hit-detection tolerance. Pixels inside the radius around the given position
* will be checked for features.
* @param {number} hitTolerance Hit tolerance in pixels.
* @api
*/
setHitTolerance(hitTolerance) {
this.hitTolerance_ = hitTolerance;
}
/**
* 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) {
const oldMap = this.getMap();
super.setMap(map);
this.updateState_(oldMap);
}
/**
* @private
*/
handleActiveChanged_() {
this.updateState_(null);
}
/**
* @param {import("../Map.js").default} oldMap Old map.
* @private
*/
updateState_(oldMap) {
let map = this.getMap();
const active = this.getActive();
if (!map || !active) {
map = map || oldMap;
if (map) {
const elem = map.getViewport();
elem.classList.remove('ol-grab', 'ol-grabbing');
}
}
}
}
export default Translate;