/** * @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 an {@link module:ol/Feature~Feature} or * {@link module:ol/render/Feature~RenderFeature} and an * {@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 an {@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 an {@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. */ 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. */ 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. */ 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. */ 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. */ 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;