leaflet-rotate 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * Handles Shift+wheel bearing changes and Ctrl+drag bearing/pitch changes.
3
+ * Handles Shift+wheel bearing changes and Ctrl+drag / right-drag bearing/pitch changes.
4
4
  *
5
5
  * @typedef L.Map.ShiftKeyRotate
6
6
  * @property {boolean} [shiftKeyRotate=true] Enable Shift+wheel bearing rotation.
@@ -20,7 +20,7 @@ L.Map.mergeOptions({
20
20
  shiftKeyRotate: true,
21
21
 
22
22
  /**
23
- * Whether Ctrl+drag changes bearing and pitch.
23
+ * Whether Ctrl+drag (or right-button drag) changes bearing and pitch.
24
24
  *
25
25
  * @type {Boolean}
26
26
  *
@@ -42,6 +42,8 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
42
42
  this._onMouseDownBound = this._onMouseDown.bind(this);
43
43
  this._onMouseMoveBound = this._onMouseMove.bind(this);
44
44
  this._onMouseUpBound = this._onMouseUp.bind(this);
45
+ this._onContextMenuBound = this._onContextMenu.bind(this);
46
+ this._applyBound = this._apply.bind(this);
45
47
  },
46
48
 
47
49
  /**
@@ -79,6 +81,7 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
79
81
  ? ['pointerdown', 'pointermove', 'pointerup pointercancel']
80
82
  : ['mousedown', 'mousemove', 'mouseup'];
81
83
  this._map._container.addEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
84
+ this._map._container.addEventListener('contextmenu', this._onContextMenuBound, true);
82
85
  this._pitchHooksAdded = true;
83
86
  },
84
87
 
@@ -90,6 +93,7 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
90
93
  removePitchHooks() {
91
94
  if (!this._pitchHooksAdded) { return; }
92
95
  this._map._container.removeEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
96
+ this._map._container.removeEventListener('contextmenu', this._onContextMenuBound, true);
93
97
  this._onMouseUp();
94
98
  delete this._pitchEvents;
95
99
  this._pitchHooksAdded = false;
@@ -113,14 +117,16 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
113
117
  },
114
118
 
115
119
  /**
116
- * Start Ctrl+drag and capture subsequent movement outside the map.
120
+ * Start Ctrl+drag (or right-button drag) and capture subsequent movement outside the map.
117
121
  *
118
122
  * @param {MouseEvent|PointerEvent} e Gesture start event.
119
123
  *
120
124
  * @since leaflet-rotate (0.5.0)
121
125
  */
122
126
  _onMouseDown(e) {
123
- if (!this._map._rotate || !this._map.options.pitchDrag || !e.ctrlKey || e.button !== 0) { return; }
127
+ if (this._dragging) { return; }
128
+ this._moved = false;
129
+ if (!this._map._rotate || !this._map.options.pitchDrag || !(e.button === 2 || (e.ctrlKey && e.button === 0))) { return; }
124
130
 
125
131
  const scrollWheelZoom = this._map.scrollWheelZoom;
126
132
  if (scrollWheelZoom && scrollWheelZoom._timer) {
@@ -133,8 +139,11 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
133
139
  e.preventDefault();
134
140
  e.stopPropagation();
135
141
  e.stopImmediatePropagation();
142
+ this._map._stopPitchBearingAnimation();
143
+ this._dragging = true;
136
144
  this._lastX = e.clientX;
137
145
  this._lastY = e.clientY;
146
+ this._pitchDelta = this._bearingDelta = 0;
138
147
  document.addEventListener(this._pitchEvents[1], this._onMouseMoveBound);
139
148
  for (const event of this._pitchEvents[2].split(' ')) {
140
149
  document.addEventListener(event, this._onMouseUpBound);
@@ -142,33 +151,75 @@ L.Map.ShiftKeyRotate = L.Handler.extend({
142
151
  },
143
152
 
144
153
  /**
145
- * Apply pointer movement to pitch (vertical) and bearing (horizontal).
154
+ * Accumulate pointer movement as pitch (vertical) and bearing (horizontal).
155
+ * Far from the horizontal center line the bearing follows the pointer angle
156
+ * around the map center.
146
157
  *
147
158
  * @param {MouseEvent|PointerEvent} e Gesture movement event.
148
159
  *
149
160
  * @since leaflet-rotate (0.5.0)
150
161
  */
151
162
  _onMouseMove(e) {
152
- this._map._setPitchAndBearing(
153
- this._map.getPitch() + (this._lastY - e.clientY) * 0.5,
154
- this._map.getBearing() + (e.clientX - this._lastX) * 0.8
155
- );
156
- this._lastX = e.clientX;
157
- this._lastY = e.clientY;
163
+ const rect = this._map._container.getBoundingClientRect(),
164
+ cx = rect.left + rect.width / 2,
165
+ cy = rect.top + rect.height / 2,
166
+ x = e.clientX,
167
+ y = e.clientY;
168
+
169
+ this._pitchDelta += (this._lastY - y) * 0.5;
170
+ this._bearingDelta += Math.abs(cy - this._lastY) > 100
171
+ ? (Math.atan2(y - cy, x - cx) - Math.atan2(y - cy, this._lastX - cx)) * (180 / Math.PI)
172
+ : (x - this._lastX) * 0.8 * (y < cy ? 1 : -1);
173
+ this._lastX = x;
174
+ this._lastY = y;
175
+ this._moved = true;
176
+ if (!this._frame) {
177
+ this._frame = requestAnimationFrame(this._applyBound);
178
+ }
158
179
  e.preventDefault();
159
180
  },
160
181
 
161
182
  /**
162
- * End Ctrl+drag and clear document-level listeners.
183
+ * Apply accumulated movement once per animation frame.
184
+ *
185
+ * @since leaflet-rotate (0.5.1)
186
+ */
187
+ _apply() {
188
+ this._frame = null;
189
+ if (!this._pitchDelta && !this._bearingDelta) { return; }
190
+ this._map._setPitchAndBearing(this._map.getPitch() + this._pitchDelta, this._map.getBearing() + this._bearingDelta);
191
+ this._pitchDelta = this._bearingDelta = 0;
192
+ },
193
+
194
+ /**
195
+ * Suppress the context menu that follows a right-button drag.
196
+ *
197
+ * @param {MouseEvent} e Context menu event.
198
+ *
199
+ * @since leaflet-rotate (0.5.1)
200
+ */
201
+ _onContextMenu(e) {
202
+ if (this._dragging || this._moved) {
203
+ e.preventDefault();
204
+ e.stopPropagation();
205
+ }
206
+ },
207
+
208
+ /**
209
+ * End Ctrl+drag, flush pending movement and clear document-level listeners.
163
210
  *
164
211
  * @since leaflet-rotate (0.5.0)
165
212
  */
166
213
  _onMouseUp() {
167
- if (!this._pitchEvents) { return; }
214
+ if (!this._pitchEvents || !this._dragging) { return; }
168
215
  document.removeEventListener(this._pitchEvents[1], this._onMouseMoveBound);
169
216
  for (const event of this._pitchEvents[2].split(' ')) {
170
217
  document.removeEventListener(event, this._onMouseUpBound);
171
218
  }
219
+ cancelAnimationFrame(this._frame);
220
+ if (this._map._mapPane) { this._apply(); }
221
+ this._frame = null;
222
+ this._dragging = false;
172
223
  delete this._lastX;
173
224
  delete this._lastY;
174
225
  },
@@ -58,6 +58,7 @@ L.Map.TouchGestures = L.Handler.extend({
58
58
  this._map = map;
59
59
  this.rotate = !!this._map.options.touchRotate;
60
60
  this.zoom = !!this._map.options.touchZoom;
61
+ this.pitch = !!this._map.options.touchPitch;
61
62
  },
62
63
 
63
64
  /**
@@ -88,12 +89,20 @@ L.Map.TouchGestures = L.Handler.extend({
88
89
  _onTouchStart(e) {
89
90
  const map = this._map;
90
91
 
91
- if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating) { return; }
92
+ if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating || this._gesture) { return; }
92
93
 
93
94
  const p1 = touchToContainerPoint(map, e.touches[0]),
94
95
  p2 = touchToContainerPoint(map, e.touches[1]),
95
96
  vector = p1.subtract(p2);
96
97
 
98
+ // Two horizontally aligned fingers moving vertically together change the pitch
99
+ const canPitch = this.pitch && map._rotate && Math.abs(vector.x) > Math.abs(vector.y);
100
+ if (!canPitch && !this.zoom && !this.rotate) { return; }
101
+ this._gesture = canPitch ? 'pending' : 'pinch';
102
+ this._startP1 = p1;
103
+ this._startP2 = p2;
104
+ map._stopPitchBearingAnimation();
105
+
97
106
  this._centerPoint = map.getSize()._divideBy(2);
98
107
  this._startLatLng = map.containerPointToLatLng(this._centerPoint);
99
108
 
@@ -136,7 +145,7 @@ L.Map.TouchGestures = L.Handler.extend({
136
145
  * @since leaflet-rotate (v0.1.0)
137
146
  */
138
147
  _onTouchMove(e) {
139
- if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) { return; }
148
+ if (!e.touches || e.touches.length !== 2 || !this._gesture) { return; }
140
149
 
141
150
  const map = this._map,
142
151
  p1 = touchToContainerPoint(map, e.touches[0]),
@@ -145,6 +154,33 @@ L.Map.TouchGestures = L.Handler.extend({
145
154
  scale = p1.distanceTo(p2) / this._startDist;
146
155
  let delta;
147
156
 
157
+ if (this._gesture === 'pending') {
158
+ const d1 = p1.subtract(this._startP1),
159
+ d2 = p2.subtract(this._startP2);
160
+ if (Math.max(d1.x * d1.x + d1.y * d1.y, d2.x * d2.x + d2.y * d2.y) < 25) {
161
+ L.DomEvent.preventDefault(e);
162
+ return;
163
+ }
164
+ const vertical = d => Math.abs(d.y) > Math.abs(d.x);
165
+ if (vertical(d1) && vertical(d2) && Math.sign(d1.y) === Math.sign(d2.y)) {
166
+ this._gesture = 'pitch';
167
+ this._lastPitchY = (this._startP1.y + this._startP2.y) / 2;
168
+ this._zooming = this._rotating = false;
169
+ } else {
170
+ this._gesture = 'pinch';
171
+ }
172
+ }
173
+
174
+ if (this._gesture === 'pitch') {
175
+ const y = (p1.y + p2.y) / 2;
176
+ map._setPitchAndBearing(map.getPitch() - (y - this._lastPitchY) * 0.5, map.getBearing());
177
+ this._lastPitchY = y;
178
+ L.DomEvent.preventDefault(e);
179
+ return;
180
+ }
181
+
182
+ if (!(this._zooming || this._rotating)) { return; }
183
+
148
184
  if (this._rotating) {
149
185
  const theta = Math.atan(vector.x / vector.y);
150
186
  let bearingDelta = (theta - this._startTheta) * (180 / Math.PI);
@@ -214,8 +250,14 @@ L.Map.TouchGestures = L.Handler.extend({
214
250
  * @since leaflet-rotate (v0.1.0)
215
251
  */
216
252
  _onTouchEnd() {
217
- if (!this._moved || !(this._zooming || this._rotating)) {
253
+ const gesture = this._gesture;
254
+ delete this._gesture;
255
+ if (gesture === 'pitch' || gesture === 'pending' || !this._moved || !(this._zooming || this._rotating)) {
218
256
  this._zooming = false;
257
+ this._rotating = false;
258
+ L.DomEvent
259
+ .off(document, 'touchmove', this._onTouchMove, this)
260
+ .off(document, 'touchend touchcancel', this._onTouchEnd, this);
219
261
  return;
220
262
  }
221
263
 
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Enables MapLibre-like two-finger pitch (both fingers dragged vertically) through the shared touch-gesture handler.
3
+ *
4
+ * @typedef L.Map.TouchPitch
5
+ * @property {boolean} [touchPitch=false] Enable two-finger map pitch (vertical drag).
6
+ *
7
+ * @since leaflet-rotate (0.5.1)
8
+ */
9
+
10
+ L.Map.mergeOptions({
11
+
12
+ /**
13
+ * Whether the map can be pitched by dragging two fingers vertically
14
+ *
15
+ * @type {Boolean}
16
+ *
17
+ * @since leaflet-rotate (0.5.1)
18
+ */
19
+ touchPitch: false,
20
+
21
+ });
22
+
23
+ L.Map.TouchPitch = L.Handler.extend({
24
+
25
+ /**
26
+ * Enable pitch handling in the map's shared touch gesture handler.
27
+ *
28
+ * @since leaflet-rotate (0.5.1)
29
+ */
30
+ addHooks() {
31
+ this._map.touchGestures.enable();
32
+ this._map.touchGestures.pitch = true;
33
+ },
34
+
35
+ /**
36
+ * Disable pitch handling without affecting pinch zoom and rotation.
37
+ *
38
+ * @since leaflet-rotate (0.5.1)
39
+ */
40
+ removeHooks() {
41
+ this._map.touchGestures.pitch = false;
42
+ },
43
+
44
+ });
45
+
46
+ /**
47
+ * Add Touch Pitch handler (disabled unless `touchGestures` is set).
48
+ *
49
+ * @property {L.Map.TouchGestures} touchGestures
50
+ *
51
+ * @since leaflet-rotate (0.5.1)
52
+ */
53
+ L.Map.addInitHook('addHandler', 'touchPitch', L.Map.TouchPitch);
@@ -13,13 +13,17 @@ Feature: Rotating a Leaflet map
13
13
  Then the default rotate control visibility follows the bearing
14
14
 
15
15
  @issue-13
16
- Scenario: Dragging the rotate control changes the map bearing
16
+ Scenario: Dragging the rotate control changes the map bearing and pitch
17
17
  When I drag the rotate control
18
- Then the map bearing follows the control drag
18
+ Then the map bearing and pitch follow the control drag
19
+
20
+ Scenario: Clicking the rotate control resets bearing and pitch
21
+ When I click the rotate control on a rotated and pitched map
22
+ Then the map animates back to north and zero pitch
19
23
 
20
24
  @issue-44
21
- Scenario: The compass control requests device-orientation permission
22
- When I activate compass mode on a permission-based device
25
+ Scenario: Enabling compass bearing requests device-orientation permission
26
+ When I enable compass bearing on a permission-based device
23
27
  Then device-orientation permission is requested
24
28
 
25
29
  @issue-7
@@ -7,6 +7,10 @@ Feature: Rotating a Leaflet map
7
7
  When I move a two-finger pinch gesture
8
8
  Then the zoom transition ends before the gesture ends
9
9
 
10
+ Scenario: Two fingers dragged vertically change the pitch, pinching does not
11
+ When I drag two fingers vertically and then pinch with touch pitch enabled
12
+ Then only the vertical two-finger drag changes the pitch
13
+
10
14
  @issue-43
11
15
  Scenario: Vector paths follow a rotated two-finger gesture
12
16
  When I rotate and zoom two fingers over a vector path
@@ -36,6 +36,11 @@ Feature: Rotating a Leaflet map
36
36
  When I drag the pitched map
37
37
  Then vector layers stay aligned with their map coordinates
38
38
 
39
+ @issue-74
40
+ Scenario: Pitched panning does not jump or reset the view on release
41
+ When I pan a highly pitched map in several directions
42
+ Then releasing the map does not jump or reset its layers
43
+
39
44
  @issue-72
40
45
  Scenario: Vector layers stay aligned after off-center zoom and rotation
41
46
  When I zoom off-center then rotate the map
@@ -64,3 +69,33 @@ Feature: Rotating a Leaflet map
64
69
  @issue-34
65
70
  Scenario: Draggable marker listeners are not duplicated
66
71
  Then the draggable marker has one drag listener and one drag-end listener
72
+
73
+ @issue-73
74
+ Scenario: Vector layers do not jump when zooming from a low level
75
+ When I animate a zoom from a low level on a rotated map
76
+ Then vector layers do not jump at the end of the zoom animation
77
+
78
+ @issue-73
79
+ Scenario: Wheel zoom keeps the cursor point on a pitched map
80
+ When I zoom around an off-center point on a pitched map
81
+ Then the point under the cursor stays in place
82
+
83
+ @issue-73
84
+ Scenario: Right-drag rotates around the map center
85
+ When I right-drag horizontally below the map center
86
+ Then the map rotates counterclockwise without opening the context menu
87
+
88
+ @issue-73
89
+ Scenario: Right-button press suppresses the context menu before movement
90
+ When a context menu event fires immediately on right-button press
91
+ Then the early context menu is suppressed
92
+
93
+ @issue-73
94
+ Scenario: High pitch keeps tile loading bounded
95
+ When I look at a high zoom map with a high pitch
96
+ Then tile loading stays bounded and covers the visible map
97
+
98
+ @issue-73
99
+ Scenario: Tiles stay aligned after panning and changing pitch
100
+ When I pan, zoom and change the pitch of a rotated map
101
+ Then the map is centered and the tiles stay aligned with the map coordinates