leaflet-rotate 0.4.0 → 0.5.1

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.
Files changed (39) hide show
  1. package/README.md +85 -5
  2. package/dist/leaflet-rotate-src.js +1325 -254
  3. package/dist/leaflet-rotate-src.js.map +4 -4
  4. package/dist/leaflet-rotate.js +1 -1
  5. package/dist/leaflet-rotate.js.map +4 -4
  6. package/lib/debug.js +62 -88
  7. package/lib/debug.min.js +16 -1
  8. package/misc/leaflet-heat-rotate.js +7 -0
  9. package/package.json +1 -1
  10. package/src/control/Rotate.js +160 -103
  11. package/src/dom/DomUtil.js +23 -12
  12. package/src/dom/Draggable.js +5 -0
  13. package/src/geo/LatLngBounds.js +5 -0
  14. package/src/geometry/Bounds.js +5 -0
  15. package/src/geometry/Point.js +17 -3
  16. package/src/index.js +4 -1
  17. package/src/layer/DivOverlay.js +34 -5
  18. package/src/layer/Popup.js +14 -3
  19. package/src/layer/Tooltip.js +16 -0
  20. package/src/layer/marker/Icon.js +12 -0
  21. package/src/layer/marker/Marker.js +86 -7
  22. package/src/layer/tile/GridLayer.js +38 -3
  23. package/src/layer/vector/Renderer.js +88 -34
  24. package/src/map/Map.js +578 -116
  25. package/src/map/handler/CompassBearing.js +34 -1
  26. package/src/map/handler/ContainerMutation.js +18 -1
  27. package/src/map/handler/ShiftKeyRotate.js +204 -13
  28. package/src/map/handler/TouchGestures.js +108 -10
  29. package/src/map/handler/TouchPitch.js +53 -0
  30. package/src/map/handler/TouchRotate.js +18 -1
  31. package/src/map/handler/TouchZoom.js +21 -1
  32. package/test/features/example.feature +8 -4
  33. package/test/features/gestures.feature +4 -0
  34. package/test/features/rotation.feature +60 -0
  35. package/test/index.html +342 -186
  36. package/test/steps/bearing.steps.js +535 -0
  37. package/test/steps/example.steps.js +56 -29
  38. package/test/steps/gestures.steps.js +55 -0
  39. package/test/support/hooks.js +4 -16
@@ -1,11 +1,19 @@
1
1
  /**
2
- * Rotates the map according to a smartphone's compass.
2
+ * Sets map bearing from device orientation when the browser exposes compass data.
3
3
  *
4
4
  * @typedef L.Map.CompassBearing
5
+ * @property {L.Map.CompassBearing} compassBearing Map handler, enabled to follow device heading.
6
+ *
7
+ * @since leaflet-rotate (v0.1.0)
5
8
  */
6
9
 
7
10
  L.Map.CompassBearing = L.Handler.extend({
8
11
 
12
+ /**
13
+ * @param {L.Map} map Map whose bearing follows device orientation.
14
+ *
15
+ * @since leaflet-rotate (v0.1.0)
16
+ */
9
17
  initialize(map) {
10
18
  this._map = map;
11
19
  /** @see https://caniuse.com/?search=DeviceOrientation */
@@ -17,9 +25,21 @@ L.Map.CompassBearing = L.Handler.extend({
17
25
  this._throttled = L.Util.throttle(this._onDeviceOrientation, 100, this);
18
26
  },
19
27
 
28
+ /**
29
+ * Subscribe to the supported device-orientation event, if available.
30
+ * On iOS, permission is requested first (call `enable()` from a user gesture).
31
+ *
32
+ * @since leaflet-rotate (v0.1.0)
33
+ */
20
34
  addHooks() {
21
35
  if (this._map._rotate && this.__deviceOrientationEvent) {
22
36
  L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this);
37
+ const DOE = window.DeviceOrientationEvent;
38
+ if (DOE && DOE.requestPermission) {
39
+ DOE.requestPermission()
40
+ .then(state => 'denied' === state && this.enabled() && this.disable())
41
+ .catch(() => {});
42
+ }
23
43
  } else {
24
44
  // L.Map.CompassBearing handler will be automatically
25
45
  // disabled if device orientation is not supported.
@@ -27,6 +47,11 @@ L.Map.CompassBearing = L.Handler.extend({
27
47
  }
28
48
  },
29
49
 
50
+ /**
51
+ * Unsubscribe from device orientation when the handler is disabled.
52
+ *
53
+ * @since leaflet-rotate (v0.1.0)
54
+ */
30
55
  removeHooks() {
31
56
  if (this._map._rotate && this.__deviceOrientationEvent) {
32
57
  L.DomEvent.off(window, this.__deviceOrientationEvent, this._throttled, this);
@@ -34,6 +59,8 @@ L.Map.CompassBearing = L.Handler.extend({
34
59
  },
35
60
 
36
61
  /**
62
+ * Apply a device orientation reading to the map bearing.
63
+ *
37
64
  * `DeviceOrientationEvent.absolute` - Indicates whether the device is providing absolute
38
65
  * orientation values (relatives to Magnetic North) or
39
66
  * using some arbitrary frame determined by the device.
@@ -50,6 +77,10 @@ L.Map.CompassBearing = L.Handler.extend({
50
77
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/absolute
51
78
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/alpha
52
79
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/orientation
80
+ *
81
+ * @param {DeviceOrientationEvent} e Orientation reading from the browser.
82
+ *
83
+ * @since leaflet-rotate (v0.1.0)
53
84
  */
54
85
  _onDeviceOrientation(e) {
55
86
  let angle = e.webkitCompassHeading != null ? e.webkitCompassHeading : e.alpha;
@@ -76,5 +107,7 @@ L.Map.CompassBearing = L.Handler.extend({
76
107
  * Add Compass bearing handler to L.Map (disabled unless `window.DeviceOrientationEvent` is set).
77
108
  *
78
109
  * @property {L.Map.CompassBearing} compassBearing
110
+ *
111
+ * @since leaflet-rotate (v0.1.0)
79
112
  */
80
113
  L.Map.addInitHook('addHandler', 'compassBearing', L.Map.CompassBearing);
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Triggers `invalidateResize` when the map's DOM container mutates.
2
+ * Calls `invalidateSize()` when the map container's inline style changes.
3
3
  *
4
4
  * @typedef L.Map.ContainerMutation
5
+ * @property {boolean} [trackContainerMutation=false] Observe inline style changes on the map container.
6
+ *
7
+ * @since leaflet-rotate (v0.1.0)
5
8
  */
6
9
 
7
10
  /**
@@ -19,6 +22,8 @@ L.Map.mergeOptions({
19
22
  *
20
23
  * @type {Boolean}
21
24
  *
25
+ * @since leaflet-rotate (v0.1.0)
26
+ *
22
27
  * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
23
28
  */
24
29
  trackContainerMutation: false
@@ -27,6 +32,11 @@ L.Map.mergeOptions({
27
32
 
28
33
  L.Map.ContainerMutation = L.Handler.extend({
29
34
 
35
+ /**
36
+ * Start observing the map container's style attribute.
37
+ *
38
+ * @since leaflet-rotate (v0.1.0)
39
+ */
30
40
  addHooks() {
31
41
  // if (!L.Browser.mutation) { return; }
32
42
  if (!this._observer) {
@@ -41,6 +51,11 @@ L.Map.ContainerMutation = L.Handler.extend({
41
51
  });
42
52
  },
43
53
 
54
+ /**
55
+ * Stop observing container mutations.
56
+ *
57
+ * @since leaflet-rotate (v0.1.0)
58
+ */
44
59
  removeHooks() {
45
60
  // if (!L.Browser.mutation) { return; }
46
61
  this._observer.disconnect();
@@ -52,5 +67,7 @@ L.Map.ContainerMutation = L.Handler.extend({
52
67
  * Add Container mutation handler to L.Map (disabled unless `trackContainerMutation` is set).
53
68
  *
54
69
  * @property {L.Map.ContainerMutation} trackContainerMutation
70
+ *
71
+ * @since leaflet-rotate (v0.1.0)
55
72
  */
56
73
  L.Map.addInitHook('addHandler', 'trackContainerMutation', L.Map.ContainerMutation);
@@ -1,55 +1,246 @@
1
1
 
2
2
  /**
3
- * Rotates the map on shift key + mousewheel scrolling (desktop).
3
+ * Handles Shift+wheel bearing changes and Ctrl+drag / right-drag bearing/pitch changes.
4
4
  *
5
5
  * @typedef L.Map.ShiftKeyRotate
6
+ * @property {boolean} [shiftKeyRotate=true] Enable Shift+wheel bearing rotation.
7
+ *
8
+ * @since leaflet-rotate (v0.1.0)
6
9
  */
7
10
 
8
11
  L.Map.mergeOptions({
9
12
 
10
13
  /**
11
- * Whether the map can be rotated with shift + wheel scroll
14
+ * Whether Shift+wheel changes the map bearing.
15
+ *
12
16
  * @type {Boolean}
17
+ *
18
+ * @since leaflet-rotate (v0.1.0)
13
19
  */
14
20
  shiftKeyRotate: true,
15
21
 
22
+ /**
23
+ * Whether Ctrl+drag (or right-button drag) changes bearing and pitch.
24
+ *
25
+ * @type {Boolean}
26
+ *
27
+ * @since leaflet-rotate (0.5.0)
28
+ */
29
+ pitchDrag: true,
30
+
16
31
  });
17
32
 
18
33
  L.Map.ShiftKeyRotate = L.Handler.extend({
19
34
 
35
+ /**
36
+ * @param {L.Map} map Map controlled by this handler.
37
+ *
38
+ * @since leaflet-rotate (v0.1.0)
39
+ */
40
+ initialize(map) {
41
+ this._map = map;
42
+ this._onMouseDownBound = this._onMouseDown.bind(this);
43
+ this._onMouseMoveBound = this._onMouseMove.bind(this);
44
+ this._onMouseUpBound = this._onMouseUp.bind(this);
45
+ this._onContextMenuBound = this._onContextMenu.bind(this);
46
+ this._applyBound = this._apply.bind(this);
47
+ },
48
+
49
+ /**
50
+ * Attach enabled wheel and pointer/mouse gesture listeners.
51
+ *
52
+ * @since leaflet-rotate (v0.1.0)
53
+ */
20
54
  addHooks() {
21
- L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
22
- // this._map.shiftKeyRotate.enable();
23
- this._map.shiftKeyRotate.rotate = true;
55
+ if (this._map.options.shiftKeyRotate) {
56
+ L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
57
+ }
58
+ if (this._map.options.pitchDrag) {
59
+ this.addPitchHooks();
60
+ }
24
61
  },
25
62
 
63
+ /**
64
+ * Detach gesture listeners and cancel any active drag.
65
+ *
66
+ * @since leaflet-rotate (v0.1.0)
67
+ */
26
68
  removeHooks() {
27
69
  L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this);
28
- this._map.shiftKeyRotate.rotate = false;
70
+ this.removePitchHooks();
71
+ },
72
+
73
+ /**
74
+ * Listen for the start of Ctrl+drag pitch gestures.
75
+ *
76
+ * @since leaflet-rotate (0.5.0)
77
+ */
78
+ addPitchHooks() {
79
+ if (this._pitchHooksAdded) { return; }
80
+ this._pitchEvents = L.version.startsWith('2.')
81
+ ? ['pointerdown', 'pointermove', 'pointerup pointercancel']
82
+ : ['mousedown', 'mousemove', 'mouseup'];
83
+ this._map._container.addEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
84
+ this._map._container.addEventListener('contextmenu', this._onContextMenuBound, true);
85
+ this._pitchHooksAdded = true;
86
+ },
87
+
88
+ /**
89
+ * Stop listening for pitch gestures and remove active document listeners.
90
+ *
91
+ * @since leaflet-rotate (0.5.0)
92
+ */
93
+ removePitchHooks() {
94
+ if (!this._pitchHooksAdded) { return; }
95
+ this._map._container.removeEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
96
+ this._map._container.removeEventListener('contextmenu', this._onContextMenuBound, true);
97
+ this._onMouseUp();
98
+ delete this._pitchEvents;
99
+ this._pitchHooksAdded = false;
29
100
  },
30
101
 
102
+ /**
103
+ * Rotate the map in fixed increments while Shift is held during wheel input.
104
+ *
105
+ * @param {WheelEvent} e Wheel event from the map container.
106
+ *
107
+ * @since leaflet-rotate (v0.1.0)
108
+ */
31
109
  _handleShiftScroll(e) {
32
110
  if (e.shiftKey) {
33
111
  e.preventDefault();
34
112
  this._map.scrollWheelZoom.disable();
35
- this._map.setBearing((this._map._bearing * (180 / Math.PI)) + Math.sign(e.deltaY) * 5);
113
+ this._map.setBearing(this._map.getBearing() + Math.sign(e.deltaY) * 5);
36
114
  } else {
37
115
  this._map.scrollWheelZoom.enable();
38
116
  }
39
117
  },
40
118
 
119
+ /**
120
+ * Start Ctrl+drag (or right-button drag) and capture subsequent movement outside the map.
121
+ *
122
+ * @param {MouseEvent|PointerEvent} e Gesture start event.
123
+ *
124
+ * @since leaflet-rotate (0.5.0)
125
+ */
126
+ _onMouseDown(e) {
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; }
130
+
131
+ const scrollWheelZoom = this._map.scrollWheelZoom;
132
+ if (scrollWheelZoom && scrollWheelZoom._timer) {
133
+ clearTimeout(scrollWheelZoom._timer);
134
+ scrollWheelZoom._timer = null;
135
+ scrollWheelZoom._delta = 0;
136
+ scrollWheelZoom._startTime = null;
137
+ }
138
+
139
+ e.preventDefault();
140
+ e.stopPropagation();
141
+ e.stopImmediatePropagation();
142
+ this._map._stopPitchBearingAnimation();
143
+ this._dragging = true;
144
+ this._lastX = e.clientX;
145
+ this._lastY = e.clientY;
146
+ this._pitchDelta = this._bearingDelta = 0;
147
+ document.addEventListener(this._pitchEvents[1], this._onMouseMoveBound);
148
+ for (const event of this._pitchEvents[2].split(' ')) {
149
+ document.addEventListener(event, this._onMouseUpBound);
150
+ }
151
+ },
152
+
153
+ /**
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.
157
+ *
158
+ * @param {MouseEvent|PointerEvent} e Gesture movement event.
159
+ *
160
+ * @since leaflet-rotate (0.5.0)
161
+ */
162
+ _onMouseMove(e) {
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
+ }
179
+ e.preventDefault();
180
+ },
181
+
182
+ /**
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.
210
+ *
211
+ * @since leaflet-rotate (0.5.0)
212
+ */
213
+ _onMouseUp() {
214
+ if (!this._pitchEvents || !this._dragging) { return; }
215
+ document.removeEventListener(this._pitchEvents[1], this._onMouseMoveBound);
216
+ for (const event of this._pitchEvents[2].split(' ')) {
217
+ document.removeEventListener(event, this._onMouseUpBound);
218
+ }
219
+ cancelAnimationFrame(this._frame);
220
+ if (this._map._mapPane) { this._apply(); }
221
+ this._frame = null;
222
+ this._dragging = false;
223
+ delete this._lastX;
224
+ delete this._lastY;
225
+ },
226
+
41
227
  });
42
228
 
43
229
  /**
44
- * Add ShiftKey handler to L.Map (enabled unless `shiftKeyRotate` is unset).
230
+ * Create the handler when either desktop rotation gesture is enabled.
45
231
  *
46
- * @property {L.Map.ShiftKeyRotate} shiftKeyRotate
232
+ * @since leaflet-rotate (v0.1.0)
47
233
  */
48
- L.Map.addInitHook('addHandler', 'shiftKeyRotate', L.Map.ShiftKeyRotate);
49
-
50
- // decrease `scrollWheelZoom` handler priority over `shiftKeyRotate` handler
51
234
  L.Map.addInitHook(function() {
52
- if (this.scrollWheelZoom.enabled() && this.shiftKeyRotate.enabled()) {
235
+ if (this.options.shiftKeyRotate) {
236
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
237
+ this.shiftKeyRotate.enable();
238
+ } else if (this.options.pitchDrag) {
239
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
240
+ this.shiftKeyRotate.addPitchHooks();
241
+ this.on('unload', this.shiftKeyRotate.removePitchHooks, this.shiftKeyRotate);
242
+ }
243
+ if (this.scrollWheelZoom.enabled() && this.options.shiftKeyRotate) {
53
244
  this.scrollWheelZoom.disable();
54
245
  this.scrollWheelZoom.enable();
55
246
  }
@@ -1,12 +1,27 @@
1
1
  /**
2
- * TouchGestures is both TouchZoom plus TouchRotate
2
+ * Shared implementation for two-finger pinch zoom and map rotation.
3
3
  *
4
4
  * @see https://github.com/fnicollet/Leaflet/commit/a77af51a6b10f308d1b9a16552091d1d0aee8834
5
5
  * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/handler/Map.TouchZoom.js
6
6
  *
7
7
  * @typedef L.Map.TouchGestures
8
+ * @property {boolean} [bounceAtZoomLimits] Whether pinch zoom may temporarily exceed zoom limits.
9
+ * @property {boolean} zoom Whether pinch zoom is enabled.
10
+ * @property {boolean} rotate Whether touch rotation is enabled.
11
+ *
12
+ * @since leaflet-rotate (v0.1.0)
8
13
  */
9
14
 
15
+ /**
16
+ * Convert a touch's client coordinates to coordinates relative to the map container.
17
+ *
18
+ * @param {L.Map} map
19
+ * @param {Touch} touch
20
+ *
21
+ * @returns {L.Point}
22
+ *
23
+ * @since leaflet-rotate (v0.1.0)
24
+ */
10
25
  function touchToContainerPoint(map, touch) {
11
26
  const container = map.getContainer(),
12
27
  rect = container.getBoundingClientRect();
@@ -20,11 +35,11 @@ function touchToContainerPoint(map, touch) {
20
35
  L.Map.mergeOptions({
21
36
 
22
37
  /**
23
- * Set it to false if you don't want the map to
24
- * zoom beyond min/max zoom and then bounce back
25
- * when pinch-zooming.
38
+ * Whether pinch zoom may temporarily exceed min/max zoom and bounce back.
26
39
  *
27
40
  * @type {Boolean}
41
+ *
42
+ * @since leaflet-rotate (v0.1.0)
28
43
  */
29
44
  bounceAtZoomLimits: true,
30
45
 
@@ -32,29 +47,62 @@ L.Map.mergeOptions({
32
47
 
33
48
  L.Map.TouchGestures = L.Handler.extend({
34
49
 
50
+ /**
51
+ * Initialize the shared handler from the map's touch zoom/rotation options.
52
+ *
53
+ * @param {L.Map} map Map whose touch gestures are handled.
54
+ *
55
+ * @since leaflet-rotate (v0.1.0)
56
+ */
35
57
  initialize(map) {
36
58
  this._map = map;
37
59
  this.rotate = !!this._map.options.touchRotate;
38
60
  this.zoom = !!this._map.options.touchZoom;
61
+ this.pitch = !!this._map.options.touchPitch;
39
62
  },
40
63
 
64
+ /**
65
+ * Listen for the start of two-finger gestures on the map container.
66
+ *
67
+ * @since leaflet-rotate (v0.1.0)
68
+ */
41
69
  addHooks() {
42
70
  L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
43
71
  },
44
72
 
73
+ /**
74
+ * Remove the touch-start listener.
75
+ *
76
+ * @since leaflet-rotate (v0.1.0)
77
+ */
45
78
  removeHooks() {
46
79
  L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
47
80
  },
48
81
 
82
+ /**
83
+ * Record initial touch points and start tracking a two-finger gesture.
84
+ *
85
+ * @param {TouchEvent} e Touch-start event.
86
+ *
87
+ * @since leaflet-rotate (v0.1.0)
88
+ */
49
89
  _onTouchStart(e) {
50
90
  const map = this._map;
51
91
 
52
- 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; }
53
93
 
54
94
  const p1 = touchToContainerPoint(map, e.touches[0]),
55
95
  p2 = touchToContainerPoint(map, e.touches[1]),
56
96
  vector = p1.subtract(p2);
57
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
+
58
106
  this._centerPoint = map.getSize()._divideBy(2);
59
107
  this._startLatLng = map.containerPointToLatLng(this._centerPoint);
60
108
 
@@ -89,8 +137,15 @@ L.Map.TouchGestures = L.Handler.extend({
89
137
  L.DomEvent.preventDefault(e);
90
138
  },
91
139
 
140
+ /**
141
+ * Update bearing and/or zoom while the two touch points move.
142
+ *
143
+ * @param {TouchEvent} e Touch-move event.
144
+ *
145
+ * @since leaflet-rotate (v0.1.0)
146
+ */
92
147
  _onTouchMove(e) {
93
- if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) { return; }
148
+ if (!e.touches || e.touches.length !== 2 || !this._gesture) { return; }
94
149
 
95
150
  const map = this._map,
96
151
  p1 = touchToContainerPoint(map, e.touches[0]),
@@ -99,6 +154,33 @@ L.Map.TouchGestures = L.Handler.extend({
99
154
  scale = p1.distanceTo(p2) / this._startDist;
100
155
  let delta;
101
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
+
102
184
  if (this._rotating) {
103
185
  const theta = Math.atan(vector.x / vector.y);
104
186
  let bearingDelta = (theta - this._startTheta) * (180 / Math.PI);
@@ -133,9 +215,12 @@ L.Map.TouchGestures = L.Handler.extend({
133
215
  delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
134
216
  if (scale === 1 && delta.x === 0 && delta.y === 0) { return; }
135
217
 
136
- const alpha = -map.getBearing() * (Math.PI / 180);
218
+ const mapDelta = map._pitch
219
+ ? map.containerPointToLayerPoint(this._centerPoint.add(delta))
220
+ .subtract(map.containerPointToLayerPoint(this._centerPoint))
221
+ : delta.rotate(-map.getBearing() * (Math.PI / 180));
137
222
 
138
- this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(delta.rotate(alpha)));
223
+ this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(mapDelta));
139
224
  }
140
225
 
141
226
  }
@@ -159,9 +244,20 @@ L.Map.TouchGestures = L.Handler.extend({
159
244
  L.DomEvent.preventDefault(e);
160
245
  },
161
246
 
247
+ /**
248
+ * Finish the gesture, settle the zoom and remove document listeners.
249
+ *
250
+ * @since leaflet-rotate (v0.1.0)
251
+ */
162
252
  _onTouchEnd() {
163
- 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)) {
164
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);
165
261
  return;
166
262
  }
167
263
 
@@ -184,8 +280,10 @@ L.Map.TouchGestures = L.Handler.extend({
184
280
  });
185
281
 
186
282
  /**
187
- * Add Touch Gestures handler (enabled unless `touchGestures` is unset).
283
+ * Add the shared touch gesture handler to the map.
188
284
  *
189
285
  * @property {L.Map.TouchGestures} touchGestures
286
+ *
287
+ * @since leaflet-rotate (v0.1.0)
190
288
  */
191
289
  L.Map.addInitHook('addHandler', 'touchGestures', L.Map.TouchGestures);
@@ -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);