UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

200 lines (159 loc) • 5.59 kB
import {Handler} from '../../core/Handler.js'; import {Draggable} from '../../dom/Draggable.js'; import {LatLngBounds} from '../../geo/LatLngBounds.js'; import {Bounds} from '../../geometry/Bounds.js'; /** @import {DragEndEvent, LeafletEvent} from '../../types.js' */ /** @import {Point} from '../../geometry/Point.js' */ // Makes the map draggable (with panning inertia), enabled by default. export class DragHandler extends Handler { addHooks() { if (!this._draggable) { const map = this._map; /** @type {Draggable} */ this._draggable = new Draggable(map._mapPane, map._container); this._draggable.on({ down: this._onDown, dragstart: this._onDragStart, drag: this._onDrag, dragend: this._onDragEnd }, this); this._draggable.on('predrag', this._onPreDragLimit, this); if (map.options.worldCopyJump) { this._draggable.on('predrag', this._onPreDragWrap, this); } } this._map._container.classList.add('leaflet-grab', 'leaflet-touch-drag'); this._draggable.enable(); /** @type {Point[]} */ this._positions = []; /** @type {number[]} */ this._times = []; } removeHooks() { this._map._container.classList.remove('leaflet-grab', 'leaflet-touch-drag'); this._draggable.disable(); } /** Returns `true` if the current or last drag moved the map, e.g. to ignore the click that ends a drag. */ moved() { return this._draggable?._moved; } /** Returns `true` while the map is being dragged. */ moving() { return this._draggable?._moving; } _onDown() { // Stop inertia before Draggable captures _startPos (#10376). this._map._stop(); } _onDragStart() { const map = this._map; map._stop(); if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) { const bounds = new LatLngBounds(this._map.options.maxBounds); /** @type {Bounds | null} */ this._offsetLimit = new Bounds( this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1), this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1) .add(this._map.getSize())); /** @type {number} */ this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity)); } else { this._offsetLimit = null; } map .fire('movestart') .fire('dragstart'); if (map.options.inertia) { this._positions = []; this._times = []; } } /** @param {LeafletEvent} e */ _onDrag(e) { if (this._map.options.inertia) { const time = Date.now(), pos = this._draggable._absPos || this._draggable._newPos; /** @type {number} */ this._lastTime = time; /** @type {Point} */ this._lastPos = pos; this._positions.push(pos); this._times.push(time); this._prunePositions(time); } this._map .fire('move', e) .fire('drag', e); } /** @param {number} time */ _prunePositions(time) { while (this._positions.length > 1 && time - this._times[0] > 50) { this._positions.shift(); this._times.shift(); } } /** * @param {number} value * @param {number} threshold */ _viscousLimit(value, threshold) { return value - (value - threshold) * this._viscosity; } _onPreDragLimit() { if (!this._viscosity || !this._offsetLimit) { return; } const offset = this._draggable._newPos.subtract(this._draggable._startPos); const limit = this._offsetLimit; if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); } if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); } if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); } if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); } this._draggable._newPos = this._draggable._startPos.add(offset); } _onPreDragWrap() { const draggable = this._draggable; draggable._absPos = draggable._newPos; draggable._newPos = this._map._wrapWorldCopy(draggable._newPos); } /** @param {DragEndEvent} e */ _onDragEnd(e) { const map = this._map, options = map.options, noInertia = !options.inertia || e.noInertia || this._times.length < 2; map.fire('dragend', e); if (noInertia) { map.fire('moveend'); } else { this._prunePositions(Date.now()); const direction = this._lastPos.subtract(this._positions[0]), duration = (this._lastTime - this._times[0]) / 1000, ease = options.easeLinearity, speedVector = direction.multiplyBy(ease / duration), speed = speedVector.distanceTo([0, 0]), limitedSpeed = Math.min(options.inertiaMaxSpeed, speed), limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed), maxOffset = map.getSize().divideBy(2); let decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease), offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round(); // with reduced motion, shorten the coast to fit within half the map size; distance grows with the // square of duration, so scaling the duration by the square root keeps the deceleration the same if (map._prefersReducedMotion() && !maxOffset.contains(offset)) { const k = Math.min(maxOffset.x / Math.abs(offset.x), maxOffset.y / Math.abs(offset.y)); offset = offset.multiplyBy(k).round(); decelerationDuration *= Math.sqrt(k); } if (!offset.x && !offset.y) { map.fire('moveend'); } else { offset = map._limitOffset(offset, map.options.maxBounds); requestAnimationFrame(() => { map.panBy(offset, { duration: decelerationDuration, easeLinearity: ease, noMoveStart: true, animate: true }); }); } } } }