UNPKG

7.25 kBJavaScriptView Raw
1import {Map} from '../Map';
2import {Handler} from '../../core/Handler';
3import {Draggable} from '../../dom/Draggable';
4import * as Util from '../../core/Util';
5import * as DomUtil from '../../dom/DomUtil';
6import {toLatLngBounds as latLngBounds} from '../../geo/LatLngBounds';
7import {toBounds} from '../../geometry/Bounds';
8
9/*
10 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
11 */
12
13// @namespace Map
14// @section Interaction Options
15Map.mergeOptions({
16 // @option dragging: Boolean = true
17 // Whether the map is draggable with mouse/touch or not.
18 dragging: true,
19
20 // @section Panning Inertia Options
21 // @option inertia: Boolean = *
22 // If enabled, panning of the map will have an inertia effect where
23 // the map builds momentum while dragging and continues moving in
24 // the same direction for some time. Feels especially nice on touch
25 // devices. Enabled by default.
26 inertia: true,
27
28 // @option inertiaDeceleration: Number = 3000
29 // The rate with which the inertial movement slows down, in pixels/second².
30 inertiaDeceleration: 3400, // px/s^2
31
32 // @option inertiaMaxSpeed: Number = Infinity
33 // Max speed of the inertial movement, in pixels/second.
34 inertiaMaxSpeed: Infinity, // px/s
35
36 // @option easeLinearity: Number = 0.2
37 easeLinearity: 0.2,
38
39 // TODO refactor, move to CRS
40 // @option worldCopyJump: Boolean = false
41 // With this option enabled, the map tracks when you pan to another "copy"
42 // of the world and seamlessly jumps to the original one so that all overlays
43 // like markers and vector layers are still visible.
44 worldCopyJump: false,
45
46 // @option maxBoundsViscosity: Number = 0.0
47 // If `maxBounds` is set, this option will control how solid the bounds
48 // are when dragging the map around. The default value of `0.0` allows the
49 // user to drag outside the bounds at normal speed, higher values will
50 // slow down map dragging outside bounds, and `1.0` makes the bounds fully
51 // solid, preventing the user from dragging outside the bounds.
52 maxBoundsViscosity: 0.0
53});
54
55export var Drag = Handler.extend({
56 addHooks: function () {
57 if (!this._draggable) {
58 var map = this._map;
59
60 this._draggable = new Draggable(map._mapPane, map._container);
61
62 this._draggable.on({
63 dragstart: this._onDragStart,
64 drag: this._onDrag,
65 dragend: this._onDragEnd
66 }, this);
67
68 this._draggable.on('predrag', this._onPreDragLimit, this);
69 if (map.options.worldCopyJump) {
70 this._draggable.on('predrag', this._onPreDragWrap, this);
71 map.on('zoomend', this._onZoomEnd, this);
72
73 map.whenReady(this._onZoomEnd, this);
74 }
75 }
76 DomUtil.addClass(this._map._container, 'leaflet-grab leaflet-touch-drag');
77 this._draggable.enable();
78 this._positions = [];
79 this._times = [];
80 },
81
82 removeHooks: function () {
83 DomUtil.removeClass(this._map._container, 'leaflet-grab');
84 DomUtil.removeClass(this._map._container, 'leaflet-touch-drag');
85 this._draggable.disable();
86 },
87
88 moved: function () {
89 return this._draggable && this._draggable._moved;
90 },
91
92 moving: function () {
93 return this._draggable && this._draggable._moving;
94 },
95
96 _onDragStart: function () {
97 var map = this._map;
98
99 map._stop();
100 if (this._map.options.maxBounds && this._map.options.maxBoundsViscosity) {
101 var bounds = latLngBounds(this._map.options.maxBounds);
102
103 this._offsetLimit = toBounds(
104 this._map.latLngToContainerPoint(bounds.getNorthWest()).multiplyBy(-1),
105 this._map.latLngToContainerPoint(bounds.getSouthEast()).multiplyBy(-1)
106 .add(this._map.getSize()));
107
108 this._viscosity = Math.min(1.0, Math.max(0.0, this._map.options.maxBoundsViscosity));
109 } else {
110 this._offsetLimit = null;
111 }
112
113 map
114 .fire('movestart')
115 .fire('dragstart');
116
117 if (map.options.inertia) {
118 this._positions = [];
119 this._times = [];
120 }
121 },
122
123 _onDrag: function (e) {
124 if (this._map.options.inertia) {
125 var time = this._lastTime = +new Date(),
126 pos = this._lastPos = this._draggable._absPos || this._draggable._newPos;
127
128 this._positions.push(pos);
129 this._times.push(time);
130
131 this._prunePositions(time);
132 }
133
134 this._map
135 .fire('move', e)
136 .fire('drag', e);
137 },
138
139 _prunePositions: function (time) {
140 while (this._positions.length > 1 && time - this._times[0] > 50) {
141 this._positions.shift();
142 this._times.shift();
143 }
144 },
145
146 _onZoomEnd: function () {
147 var pxCenter = this._map.getSize().divideBy(2),
148 pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);
149
150 this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
151 this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
152 },
153
154 _viscousLimit: function (value, threshold) {
155 return value - (value - threshold) * this._viscosity;
156 },
157
158 _onPreDragLimit: function () {
159 if (!this._viscosity || !this._offsetLimit) { return; }
160
161 var offset = this._draggable._newPos.subtract(this._draggable._startPos);
162
163 var limit = this._offsetLimit;
164 if (offset.x < limit.min.x) { offset.x = this._viscousLimit(offset.x, limit.min.x); }
165 if (offset.y < limit.min.y) { offset.y = this._viscousLimit(offset.y, limit.min.y); }
166 if (offset.x > limit.max.x) { offset.x = this._viscousLimit(offset.x, limit.max.x); }
167 if (offset.y > limit.max.y) { offset.y = this._viscousLimit(offset.y, limit.max.y); }
168
169 this._draggable._newPos = this._draggable._startPos.add(offset);
170 },
171
172 _onPreDragWrap: function () {
173 // TODO refactor to be able to adjust map pane position after zoom
174 var worldWidth = this._worldWidth,
175 halfWidth = Math.round(worldWidth / 2),
176 dx = this._initialWorldOffset,
177 x = this._draggable._newPos.x,
178 newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
179 newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
180 newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;
181
182 this._draggable._absPos = this._draggable._newPos.clone();
183 this._draggable._newPos.x = newX;
184 },
185
186 _onDragEnd: function (e) {
187 var map = this._map,
188 options = map.options,
189
190 noInertia = !options.inertia || e.noInertia || this._times.length < 2;
191
192 map.fire('dragend', e);
193
194 if (noInertia) {
195 map.fire('moveend');
196
197 } else {
198 this._prunePositions(+new Date());
199
200 var direction = this._lastPos.subtract(this._positions[0]),
201 duration = (this._lastTime - this._times[0]) / 1000,
202 ease = options.easeLinearity,
203
204 speedVector = direction.multiplyBy(ease / duration),
205 speed = speedVector.distanceTo([0, 0]),
206
207 limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
208 limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),
209
210 decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
211 offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();
212
213 if (!offset.x && !offset.y) {
214 map.fire('moveend');
215
216 } else {
217 offset = map._limitOffset(offset, map.options.maxBounds);
218
219 Util.requestAnimFrame(function () {
220 map.panBy(offset, {
221 duration: decelerationDuration,
222 easeLinearity: ease,
223 noMoveStart: true,
224 animate: true
225 });
226 });
227 }
228 }
229 }
230});
231
232// @section Handlers
233// @property dragging: Handler
234// Map dragging handler (by both mouse and touch).
235Map.addInitHook('addHandler', 'dragging', Drag);