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.
- package/README.md +17 -6
- package/dist/leaflet-rotate-src.js +606 -168
- package/dist/leaflet-rotate-src.js.map +4 -4
- package/dist/leaflet-rotate.js +1 -1
- package/dist/leaflet-rotate.js.map +4 -4
- package/lib/debug.js +62 -88
- package/lib/debug.min.js +16 -1
- package/package.json +1 -1
- package/src/control/Rotate.js +102 -118
- package/src/index.js +1 -0
- package/src/layer/tile/GridLayer.js +20 -1
- package/src/layer/vector/Renderer.js +37 -34
- package/src/map/Map.js +369 -72
- package/src/map/handler/CompassBearing.js +7 -0
- package/src/map/handler/ShiftKeyRotate.js +64 -13
- package/src/map/handler/TouchGestures.js +45 -3
- package/src/map/handler/TouchPitch.js +53 -0
- package/test/features/example.feature +8 -4
- package/test/features/gestures.feature +4 -0
- package/test/features/rotation.feature +35 -0
- package/test/index.html +559 -185
- package/test/steps/example.steps.js +56 -29
- package/test/steps/gestures.steps.js +55 -0
- package/test/steps/{bearing.steps.js → rotation.steps.js} +312 -15
|
@@ -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 (
|
|
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
|
-
*
|
|
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.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
*
|
|
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 || !
|
|
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
|
-
|
|
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
|
|
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:
|
|
22
|
-
When I
|
|
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
|