leaflet-rotate 0.4.0 → 0.5.0

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,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,195 @@
1
1
 
2
2
  /**
3
- * Rotates the map on shift key + mousewheel scrolling (desktop).
3
+ * Handles Shift+wheel bearing changes and Ctrl+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 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
+ },
46
+
47
+ /**
48
+ * Attach enabled wheel and pointer/mouse gesture listeners.
49
+ *
50
+ * @since leaflet-rotate (v0.1.0)
51
+ */
20
52
  addHooks() {
21
- L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
22
- // this._map.shiftKeyRotate.enable();
23
- this._map.shiftKeyRotate.rotate = true;
53
+ if (this._map.options.shiftKeyRotate) {
54
+ L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
55
+ }
56
+ if (this._map.options.pitchDrag) {
57
+ this.addPitchHooks();
58
+ }
24
59
  },
25
60
 
61
+ /**
62
+ * Detach gesture listeners and cancel any active drag.
63
+ *
64
+ * @since leaflet-rotate (v0.1.0)
65
+ */
26
66
  removeHooks() {
27
67
  L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this);
28
- this._map.shiftKeyRotate.rotate = false;
68
+ this.removePitchHooks();
69
+ },
70
+
71
+ /**
72
+ * Listen for the start of Ctrl+drag pitch gestures.
73
+ *
74
+ * @since leaflet-rotate (0.5.0)
75
+ */
76
+ addPitchHooks() {
77
+ if (this._pitchHooksAdded) { return; }
78
+ this._pitchEvents = L.version.startsWith('2.')
79
+ ? ['pointerdown', 'pointermove', 'pointerup pointercancel']
80
+ : ['mousedown', 'mousemove', 'mouseup'];
81
+ this._map._container.addEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
82
+ this._pitchHooksAdded = true;
83
+ },
84
+
85
+ /**
86
+ * Stop listening for pitch gestures and remove active document listeners.
87
+ *
88
+ * @since leaflet-rotate (0.5.0)
89
+ */
90
+ removePitchHooks() {
91
+ if (!this._pitchHooksAdded) { return; }
92
+ this._map._container.removeEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
93
+ this._onMouseUp();
94
+ delete this._pitchEvents;
95
+ this._pitchHooksAdded = false;
29
96
  },
30
97
 
98
+ /**
99
+ * Rotate the map in fixed increments while Shift is held during wheel input.
100
+ *
101
+ * @param {WheelEvent} e Wheel event from the map container.
102
+ *
103
+ * @since leaflet-rotate (v0.1.0)
104
+ */
31
105
  _handleShiftScroll(e) {
32
106
  if (e.shiftKey) {
33
107
  e.preventDefault();
34
108
  this._map.scrollWheelZoom.disable();
35
- this._map.setBearing((this._map._bearing * (180 / Math.PI)) + Math.sign(e.deltaY) * 5);
109
+ this._map.setBearing(this._map.getBearing() + Math.sign(e.deltaY) * 5);
36
110
  } else {
37
111
  this._map.scrollWheelZoom.enable();
38
112
  }
39
113
  },
40
114
 
115
+ /**
116
+ * Start Ctrl+drag and capture subsequent movement outside the map.
117
+ *
118
+ * @param {MouseEvent|PointerEvent} e Gesture start event.
119
+ *
120
+ * @since leaflet-rotate (0.5.0)
121
+ */
122
+ _onMouseDown(e) {
123
+ if (!this._map._rotate || !this._map.options.pitchDrag || !e.ctrlKey || e.button !== 0) { return; }
124
+
125
+ const scrollWheelZoom = this._map.scrollWheelZoom;
126
+ if (scrollWheelZoom && scrollWheelZoom._timer) {
127
+ clearTimeout(scrollWheelZoom._timer);
128
+ scrollWheelZoom._timer = null;
129
+ scrollWheelZoom._delta = 0;
130
+ scrollWheelZoom._startTime = null;
131
+ }
132
+
133
+ e.preventDefault();
134
+ e.stopPropagation();
135
+ e.stopImmediatePropagation();
136
+ this._lastX = e.clientX;
137
+ this._lastY = e.clientY;
138
+ document.addEventListener(this._pitchEvents[1], this._onMouseMoveBound);
139
+ for (const event of this._pitchEvents[2].split(' ')) {
140
+ document.addEventListener(event, this._onMouseUpBound);
141
+ }
142
+ },
143
+
144
+ /**
145
+ * Apply pointer movement to pitch (vertical) and bearing (horizontal).
146
+ *
147
+ * @param {MouseEvent|PointerEvent} e Gesture movement event.
148
+ *
149
+ * @since leaflet-rotate (0.5.0)
150
+ */
151
+ _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;
158
+ e.preventDefault();
159
+ },
160
+
161
+ /**
162
+ * End Ctrl+drag and clear document-level listeners.
163
+ *
164
+ * @since leaflet-rotate (0.5.0)
165
+ */
166
+ _onMouseUp() {
167
+ if (!this._pitchEvents) { return; }
168
+ document.removeEventListener(this._pitchEvents[1], this._onMouseMoveBound);
169
+ for (const event of this._pitchEvents[2].split(' ')) {
170
+ document.removeEventListener(event, this._onMouseUpBound);
171
+ }
172
+ delete this._lastX;
173
+ delete this._lastY;
174
+ },
175
+
41
176
  });
42
177
 
43
178
  /**
44
- * Add ShiftKey handler to L.Map (enabled unless `shiftKeyRotate` is unset).
179
+ * Create the handler when either desktop rotation gesture is enabled.
45
180
  *
46
- * @property {L.Map.ShiftKeyRotate} shiftKeyRotate
181
+ * @since leaflet-rotate (v0.1.0)
47
182
  */
48
- L.Map.addInitHook('addHandler', 'shiftKeyRotate', L.Map.ShiftKeyRotate);
49
-
50
- // decrease `scrollWheelZoom` handler priority over `shiftKeyRotate` handler
51
183
  L.Map.addInitHook(function() {
52
- if (this.scrollWheelZoom.enabled() && this.shiftKeyRotate.enabled()) {
184
+ if (this.options.shiftKeyRotate) {
185
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
186
+ this.shiftKeyRotate.enable();
187
+ } else if (this.options.pitchDrag) {
188
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
189
+ this.shiftKeyRotate.addPitchHooks();
190
+ this.on('unload', this.shiftKeyRotate.removePitchHooks, this.shiftKeyRotate);
191
+ }
192
+ if (this.scrollWheelZoom.enabled() && this.options.shiftKeyRotate) {
53
193
  this.scrollWheelZoom.disable();
54
194
  this.scrollWheelZoom.enable();
55
195
  }
@@ -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,20 +47,44 @@ 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;
39
61
  },
40
62
 
63
+ /**
64
+ * Listen for the start of two-finger gestures on the map container.
65
+ *
66
+ * @since leaflet-rotate (v0.1.0)
67
+ */
41
68
  addHooks() {
42
69
  L.DomEvent.on(this._map._container, 'touchstart', this._onTouchStart, this);
43
70
  },
44
71
 
72
+ /**
73
+ * Remove the touch-start listener.
74
+ *
75
+ * @since leaflet-rotate (v0.1.0)
76
+ */
45
77
  removeHooks() {
46
78
  L.DomEvent.off(this._map._container, 'touchstart', this._onTouchStart, this);
47
79
  },
48
80
 
81
+ /**
82
+ * Record initial touch points and start tracking a two-finger gesture.
83
+ *
84
+ * @param {TouchEvent} e Touch-start event.
85
+ *
86
+ * @since leaflet-rotate (v0.1.0)
87
+ */
49
88
  _onTouchStart(e) {
50
89
  const map = this._map;
51
90
 
@@ -89,6 +128,13 @@ L.Map.TouchGestures = L.Handler.extend({
89
128
  L.DomEvent.preventDefault(e);
90
129
  },
91
130
 
131
+ /**
132
+ * Update bearing and/or zoom while the two touch points move.
133
+ *
134
+ * @param {TouchEvent} e Touch-move event.
135
+ *
136
+ * @since leaflet-rotate (v0.1.0)
137
+ */
92
138
  _onTouchMove(e) {
93
139
  if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) { return; }
94
140
 
@@ -133,9 +179,12 @@ L.Map.TouchGestures = L.Handler.extend({
133
179
  delta = p1._add(p2)._divideBy(2)._subtract(this._centerPoint);
134
180
  if (scale === 1 && delta.x === 0 && delta.y === 0) { return; }
135
181
 
136
- const alpha = -map.getBearing() * (Math.PI / 180);
182
+ const mapDelta = map._pitch
183
+ ? map.containerPointToLayerPoint(this._centerPoint.add(delta))
184
+ .subtract(map.containerPointToLayerPoint(this._centerPoint))
185
+ : delta.rotate(-map.getBearing() * (Math.PI / 180));
137
186
 
138
- this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(delta.rotate(alpha)));
187
+ this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(mapDelta));
139
188
  }
140
189
 
141
190
  }
@@ -159,6 +208,11 @@ L.Map.TouchGestures = L.Handler.extend({
159
208
  L.DomEvent.preventDefault(e);
160
209
  },
161
210
 
211
+ /**
212
+ * Finish the gesture, settle the zoom and remove document listeners.
213
+ *
214
+ * @since leaflet-rotate (v0.1.0)
215
+ */
162
216
  _onTouchEnd() {
163
217
  if (!this._moved || !(this._zooming || this._rotating)) {
164
218
  this._zooming = false;
@@ -184,8 +238,10 @@ L.Map.TouchGestures = L.Handler.extend({
184
238
  });
185
239
 
186
240
  /**
187
- * Add Touch Gestures handler (enabled unless `touchGestures` is unset).
241
+ * Add the shared touch gesture handler to the map.
188
242
  *
189
243
  * @property {L.Map.TouchGestures} touchGestures
244
+ *
245
+ * @since leaflet-rotate (v0.1.0)
190
246
  */
191
247
  L.Map.addInitHook('addHandler', 'touchGestures', L.Map.TouchGestures);
@@ -1,7 +1,10 @@
1
1
  /**
2
- * Rotates the map on two-finger (touch devices).
2
+ * Enables two-finger rotation through the shared touch-gesture handler.
3
3
  *
4
4
  * @typedef L.Map.TouchRotate
5
+ * @property {boolean} [touchRotate=false] Enable two-finger map rotation.
6
+ *
7
+ * @since leaflet-rotate (v0.1.0)
5
8
  */
6
9
 
7
10
  L.Map.mergeOptions({
@@ -10,6 +13,8 @@ L.Map.mergeOptions({
10
13
  * Whether the map can be rotated with a two-finger rotation gesture
11
14
  *
12
15
  * @type {Boolean}
16
+ *
17
+ * @since leaflet-rotate (v0.1.0)
13
18
  */
14
19
  touchRotate: false,
15
20
 
@@ -17,11 +22,21 @@ L.Map.mergeOptions({
17
22
 
18
23
  L.Map.TouchRotate = L.Handler.extend({
19
24
 
25
+ /**
26
+ * Enable rotation handling in the map's shared touch gesture handler.
27
+ *
28
+ * @since leaflet-rotate (v0.1.0)
29
+ */
20
30
  addHooks() {
21
31
  this._map.touchGestures.enable();
22
32
  this._map.touchGestures.rotate = true;
23
33
  },
24
34
 
35
+ /**
36
+ * Disable rotation handling without affecting pinch zoom.
37
+ *
38
+ * @since leaflet-rotate (v0.1.0)
39
+ */
25
40
  removeHooks() {
26
41
  this._map.touchGestures.rotate = false;
27
42
  },
@@ -32,5 +47,7 @@ L.Map.TouchRotate = L.Handler.extend({
32
47
  * Add Touch Rotate handler (disabled unless `touchGestures` is set).
33
48
  *
34
49
  * @property {L.Map.TouchGestures} touchGestures
50
+ *
51
+ * @since leaflet-rotate (v0.1.0)
35
52
  */
36
53
  L.Map.addInitHook('addHandler', 'touchRotate', L.Map.TouchRotate);
@@ -1,9 +1,13 @@
1
1
  /**
2
- * Adds pinch zoom rotation on mobile browsers
2
+ * Adds pinch zoom support through the shared touch-gesture handler.
3
3
  *
4
4
  * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/handler/Map.TouchZoom.js
5
5
  *
6
6
  * @external L.Map.TouchZoom
7
+ * @property {boolean|string} [touchZoom=L.Browser.touch] Enable pinch zoom; `'center'` zooms around the view center.
8
+ * @property {boolean} [bounceAtZoomLimits=false] Allow pinch zoom to exceed zoom limits before settling.
9
+ *
10
+ * @since leaflet-rotate (v0.1.0)
7
11
  */
8
12
 
9
13
  L.Map.mergeOptions({
@@ -16,6 +20,8 @@ L.Map.mergeOptions({
16
20
  * for touch-capable web browsers.
17
21
  *
18
22
  * @type {(Boolean|String)}
23
+ *
24
+ * @since leaflet-rotate (v0.1.0)
19
25
  */
20
26
  touchZoom: L.Browser.touch,
21
27
 
@@ -27,6 +33,8 @@ L.Map.mergeOptions({
27
33
  * when pinch-zooming.
28
34
  *
29
35
  * @type {Boolean}
36
+ *
37
+ * @since leaflet-rotate (v0.1.0)
30
38
  */
31
39
  bounceAtZoomLimits: false,
32
40
 
@@ -34,12 +42,22 @@ L.Map.mergeOptions({
34
42
 
35
43
  L.Map.TouchZoom = L.Handler.extend({
36
44
 
45
+ /**
46
+ * Enable pinch zoom handling and mark the shared touch handler as active.
47
+ *
48
+ * @since leaflet-rotate (v0.1.0)
49
+ */
37
50
  addHooks() {
38
51
  this._map._container.classList.add('leaflet-touch-zoom');
39
52
  this._map.touchGestures.enable();
40
53
  this._map.touchGestures.zoom = true;
41
54
  },
42
55
 
56
+ /**
57
+ * Disable pinch zoom handling while preserving touch rotation state.
58
+ *
59
+ * @since leaflet-rotate (v0.1.0)
60
+ */
43
61
  removeHooks() {
44
62
  this._map._container.classList.remove('leaflet-touch-zoom');
45
63
  this._map.touchGestures.zoom = false;
@@ -51,5 +69,7 @@ L.Map.TouchZoom = L.Handler.extend({
51
69
  * Add Touch Zoom handler (disabled unless `L.Browser.touch` is set).
52
70
  *
53
71
  * @property {L.Map.TouchGestures} touchGestures
72
+ *
73
+ * @since leaflet-rotate (v0.1.0)
54
74
  */
55
75
  L.Map.addInitHook('addHandler', 'touchZoom', L.Map.TouchZoom);
@@ -21,6 +21,36 @@ Feature: Rotating a Leaflet map
21
21
  When I animate the bearing to 90 degrees
22
22
  Then the bearing reaches 90 degrees gradually
23
23
 
24
+ @issue-72
25
+ Scenario: Pitch projects and reverses map coordinates
26
+ When I set the map pitch to 45 degrees
27
+ Then map coordinates stay consistent with the pitched view
28
+
29
+ @issue-72
30
+ Scenario: Ctrl-drag changes pitch and bearing without panning
31
+ When I change pitch and bearing with Ctrl-drag
32
+ Then pitch and bearing change without moving the map pane
33
+
34
+ @issue-72
35
+ Scenario: Pitched vector layers stay aligned while panning
36
+ When I drag the pitched map
37
+ Then vector layers stay aligned with their map coordinates
38
+
39
+ @issue-72
40
+ Scenario: Vector layers stay aligned after off-center zoom and rotation
41
+ When I zoom off-center then rotate the map
42
+ Then vector layers stay aligned with their map coordinates
43
+
44
+ @issue-72
45
+ Scenario: Ctrl-drag after panning keeps the map center stable
46
+ When I pan the map then Ctrl-drag
47
+ Then the map center stays stable during Ctrl-drag
48
+
49
+ @issue-72
50
+ Scenario: Ctrl-drag cancels a pending wheel zoom
51
+ When I Ctrl-drag while a wheel zoom is pending
52
+ Then the pending wheel zoom does not move the map
53
+
24
54
  @issue-25
25
55
  Scenario: Fit bounds zoom is stable
26
56
  When I fit the same bounds twice
package/test/index.html CHANGED
@@ -83,7 +83,8 @@
83
83
  : 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
84
84
 
85
85
  if (version === '2') {
86
- globalThis.L = await import('https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.js');
86
+ await loadScript('https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet-global-src.js');
87
+ globalThis.L = globalThis.leaflet;
87
88
  } else {
88
89
  await loadScript('https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.js');
89
90
  }