leaflet-rotate 0.3.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.
Files changed (42) hide show
  1. package/README.md +74 -5
  2. package/build/test.js +18 -0
  3. package/dist/leaflet-rotate-src.js +1081 -303
  4. package/dist/leaflet-rotate-src.js.map +3 -3
  5. package/dist/leaflet-rotate.js +1 -1
  6. package/dist/leaflet-rotate.js.map +3 -3
  7. package/lib/debug.js +84 -66
  8. package/lib/debug.min.js +1 -1
  9. package/misc/leaflet-heat-rotate.js +14 -7
  10. package/misc/loremIpsum.js +1 -1
  11. package/misc/places.js +1 -1
  12. package/misc/route.js +1 -1
  13. package/package.json +5 -5
  14. package/src/control/Rotate.js +98 -29
  15. package/src/dom/DomUtil.js +42 -47
  16. package/src/dom/Draggable.js +6 -1
  17. package/src/geo/LatLngBounds.js +5 -0
  18. package/src/geometry/Bounds.js +5 -0
  19. package/src/geometry/Point.js +33 -10
  20. package/src/index.js +3 -1
  21. package/src/layer/DivOverlay.js +43 -12
  22. package/src/layer/Popup.js +29 -16
  23. package/src/layer/Tooltip.js +21 -5
  24. package/src/layer/marker/Icon.js +20 -8
  25. package/src/layer/marker/Marker.js +103 -24
  26. package/src/layer/tile/GridLayer.js +22 -6
  27. package/src/layer/vector/Renderer.js +102 -11
  28. package/src/map/Map.js +408 -121
  29. package/src/map/handler/CompassBearing.js +33 -7
  30. package/src/map/handler/ContainerMutation.js +20 -3
  31. package/src/map/handler/ShiftKeyRotate.js +156 -16
  32. package/src/map/handler/TouchGestures.js +91 -29
  33. package/src/map/handler/TouchRotate.js +20 -3
  34. package/src/map/handler/TouchZoom.js +25 -5
  35. package/test/features/example.feature +5 -0
  36. package/test/features/rotation.feature +30 -0
  37. package/test/index.html +60 -22
  38. package/test/steps/bearing.steps.js +345 -3
  39. package/test/steps/example.steps.js +72 -11
  40. package/test/steps/gestures.steps.js +4 -4
  41. package/test/steps/overlays.steps.js +32 -32
  42. package/test/support/hooks.js +38 -2
@@ -3,6 +3,10 @@ var leafletRotate = (() => {
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
6
10
  var __copyProps = (to, from, except, desc) => {
7
11
  if (from && typeof from === "object" || typeof from === "function") {
8
12
  for (let key of __getOwnPropNames(from))
@@ -15,84 +19,92 @@ var leafletRotate = (() => {
15
19
 
16
20
  // src/index.js
17
21
  var index_exports = {};
22
+ __export(index_exports, {
23
+ toPoint: () => toPoint
24
+ });
18
25
 
19
26
  // src/dom/DomUtil.js
20
- var domUtilProto = L.extend({}, L.DomUtil);
21
- L.extend(L.DomUtil, {
22
- /**
23
- * Resets the 3D CSS transform of `el` so it is
24
- * translated by `offset` pixels and optionally
25
- * scaled by `scale`. Does not have an effect if
26
- * the browser doesn't support 3D CSS transforms.
27
- *
28
- * @param {HTMLElement} el
29
- * @param {L.Point} offset
30
- * @param {Number} scale
31
- * @param {Number} bearing
32
- * @param {L.Point} pivot
33
- */
34
- setTransform: function(el, offset, scale, bearing, pivot) {
35
- var pos = offset || new L.Point(0, 0);
27
+ var domUtilProto = Object.assign({}, L.DomUtil);
28
+ Object.assign(L.DomUtil, {
29
+ /**
30
+ * Resets the 3D CSS transform of `el` so it is translated by `offset` pixels,
31
+ * optionally scaled by `scale`, and rotated around `pivot` when `bearing` is set.
32
+ *
33
+ * @param {HTMLElement} el Element to transform.
34
+ * @param {L.Point} [offset] Translation in pixels; defaults to the origin.
35
+ * @param {number} [scale] Optional scale factor.
36
+ * @param {number} [bearing] Optional rotation in radians.
37
+ * @param {L.Point} [pivot] Point in pixels around which to rotate.
38
+ *
39
+ * @returns {void}
40
+ *
41
+ * @since leaflet-rotate (v0.1.0)
42
+ */
43
+ setTransform(el, offset, scale, bearing, pivot) {
44
+ let pos = offset || new L.Point(0, 0);
36
45
  if (!bearing) {
37
- offset = pos._round();
38
- return domUtilProto.setTransform.apply(this, arguments);
46
+ return domUtilProto.setTransform(el, pos._round(), scale);
39
47
  }
40
48
  pos = pos.rotateFrom(bearing, pivot);
41
- el.style[L.DomUtil.TRANSFORM] = "translate3d(" + pos.x + "px," + pos.y + "px,0)" + (scale ? " scale(" + scale + ")" : "") + " rotate(" + bearing + "rad)";
49
+ el.style.transform = "translate3d(" + pos.x + "px," + pos.y + "px,0)" + (scale ? " scale(" + scale + ")" : "") + " rotate(" + bearing + "rad)";
42
50
  },
43
51
  /**
44
- * Sets the position of `el` to coordinates specified by
45
- * `position`, using CSS translate or top/left positioning
46
- * depending on the browser (used by Leaflet internally
47
- * to position its layers).
52
+ * Sets the position of `el` to `point`, using CSS transforms or top/left positioning
53
+ * depending on browser support, and optionally rotating it around `pivot`.
54
+ *
55
+ * @param {HTMLElement} el Element to position.
56
+ * @param {L.Point} point Position in pixels.
57
+ * @param {number} [bearing] Rotation in radians; zero or omitted uses Leaflet positioning.
58
+ * @param {L.Point} [pivot] Point in pixels around which to rotate.
59
+ * @param {number} [scale] Optional scale factor.
60
+ *
61
+ * @returns {void}
48
62
  *
49
- * @param {HTMLElement} el
50
- * @param {L.Point} point
51
- * @param {Number} bearing
52
- * @param {L.Point} pivot
53
- * @param {Number} scale
63
+ * @since leaflet-rotate (v0.1.0)
54
64
  */
55
- setPosition: function(el, point, bearing, pivot, scale) {
65
+ setPosition(el, point, bearing, pivot, scale) {
56
66
  if (!bearing) {
57
- return domUtilProto.setPosition.apply(this, arguments);
67
+ return domUtilProto.setPosition(el, point);
58
68
  }
59
69
  el._leaflet_pos = point;
60
- if (L.Browser.any3d) {
61
- L.DomUtil.setTransform(el, point, scale, bearing, pivot);
62
- } else {
63
- el.style.left = point.x + "px";
64
- el.style.top = point.y + "px";
65
- }
70
+ L.DomUtil.setTransform(el, point, scale, bearing, pivot);
66
71
  },
67
72
  /**
68
- * @constant radians = degrees × π/180°
69
- */
70
- DEG_TO_RAD: Math.PI / 180,
71
- /**
72
- * @constant degrees = radians × 180°/π
73
+ * Returns the position assigned to `el`.
74
+ *
75
+ * @param {HTMLElement} el Element whose Leaflet position is requested.
76
+ *
77
+ * @returns {L.Point} Stored Leaflet position, or the position measured by Leaflet.
78
+ *
79
+ * @since leaflet-rotate (v0.1.0)
73
80
  */
74
- RAD_TO_DEG: 180 / Math.PI
81
+ getPosition(el) {
82
+ return el._leaflet_pos || domUtilProto.getPosition(el);
83
+ }
75
84
  });
76
85
 
77
86
  // src/dom/Draggable.js
78
87
  L.Draggable.include({
79
88
  /** @TODO */
80
- // updateMapBearing: function(mapBearing) {
89
+ // updateMapBearing(mapBearing) {
81
90
  // this._mapBearing = mapBearing;
82
91
  // },
83
92
  });
84
93
 
85
94
  // src/geometry/Point.js
86
- L.extend(L.Point.prototype, {
95
+ Object.assign(L.Point.prototype, {
87
96
  /**
88
97
  * Rotate around (0,0) by applying the 2D rotation matrix:
89
98
  *
90
99
  * ⎡ x' ⎤ = ⎡ cos θ -sin θ ⎤ ⎡ x ⎤
91
100
  * ⎣ y' ⎦ ⎣ sin θ cos θ ⎦ ⎣ y ⎦
92
101
  *
93
- * @param theta must be given in radians.
102
+ * @param {number} theta Rotation angle in radians, counterclockwise.
103
+ * @returns {L.Point} Rotated point (or this point when theta is zero).
104
+ *
105
+ * @since leaflet-rotate (v0.1.0)
94
106
  */
95
- rotate: function(theta) {
107
+ rotate(theta) {
96
108
  return this.rotateFrom(theta, new L.Point(0, 0));
97
109
  },
98
110
  /**
@@ -104,53 +116,85 @@ var leafletRotate = (() => {
104
116
  *
105
117
  * same as `this.subtract(pivot).rotate(theta).add(pivot)`
106
118
  *
107
- * @param {Number} theta
108
- * @param {L.Point} pivot
119
+ * @param {number} theta Rotation angle in radians, counterclockwise.
120
+ * @param {L.Point} pivot Point to rotate around.
109
121
  *
110
122
  * @returns {L.Point}
123
+ *
124
+ * @since leaflet-rotate (v0.1.0)
111
125
  */
112
- rotateFrom: function(theta, pivot) {
126
+ rotateFrom(theta, pivot) {
113
127
  if (!theta) {
114
128
  return this;
115
129
  }
116
- var sinTheta = Math.sin(theta);
117
- var cosTheta = Math.cos(theta);
118
- var cx = pivot.x, cy = pivot.y;
119
- var x = this.x - cx, y = this.y - cy;
130
+ const sinTheta = Math.sin(theta);
131
+ const cosTheta = Math.cos(theta);
132
+ const cx = pivot.x, cy = pivot.y;
133
+ const x = this.x - cx, y = this.y - cy;
120
134
  return new L.Point(
121
135
  x * cosTheta - y * sinTheta + cx,
122
136
  x * sinTheta + y * cosTheta + cy
123
137
  );
124
138
  }
125
139
  });
140
+ function toPoint(point) {
141
+ if (point == null || point instanceof L.Point) {
142
+ return point;
143
+ }
144
+ return Array.isArray(point) ? new L.Point(point[0], point[1]) : new L.Point(point.x, point.y);
145
+ }
126
146
 
127
147
  // src/layer/DivOverlay.js
128
- var divOverlayProto = L.extend({}, L.DivOverlay.prototype);
148
+ var divOverlayProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.DivOverlay.prototype));
129
149
  L.DivOverlay.include({
130
150
  /**
131
- * Update L.Popup and L.Tooltip anchor positions after
132
- * the map is moved by calling `map.setBearing(theta)`
151
+ * Extend overlay event subscriptions to refresh position after map rotation and pitch.
133
152
  *
134
153
  * @listens L.Map~rotate
154
+ * @listens L.Map~pitch
155
+ *
156
+ * @returns {Object} Leaflet event map for the overlay.
157
+ *
158
+ * @since leaflet-rotate (v0.1.0)
159
+ */
160
+ getEvents() {
161
+ const events = Object.assign(divOverlayProto.getEvents.apply(this, arguments), {
162
+ rotate: this._updatePosition,
163
+ pitch: this._updatePitchEvents
164
+ });
165
+ if (this._map && this._map._pitch) {
166
+ events.move = this._updatePosition;
167
+ }
168
+ return events;
169
+ },
170
+ /**
171
+ * Listen to map movement only while pitch is non-zero.
172
+ *
173
+ * @since leaflet-rotate (0.5.0)
135
174
  */
136
- getEvents: function() {
137
- return L.extend(divOverlayProto.getEvents.apply(this, arguments), { rotate: this._updatePosition });
175
+ _updatePitchEvents() {
176
+ if (this._map._pitch) {
177
+ this._map.on("move", this._updatePosition, this);
178
+ } else {
179
+ this._map.off("move", this._updatePosition, this);
180
+ }
138
181
  },
139
182
  /**
140
- * 0. update element anchor point (divOverlayProto v1.9.3)
141
- * 1. rotate around anchor point (subtract anchor -> rotate point -> add anchor)
183
+ * Reposition the overlay after map movement, applying rotation/pitch-aware coordinates.
184
+ *
185
+ * @since leaflet-rotate (v0.1.0)
142
186
  */
143
- _updatePosition: function() {
187
+ _updatePosition() {
144
188
  if (!this._map) {
145
189
  return;
146
190
  }
147
191
  divOverlayProto._updatePosition.apply(this, arguments);
148
192
  if (this._map._rotate && this._zoomAnimated) {
149
- var anchor = this._getAnchor();
150
- var pos = L.DomUtil.getPosition(this._container).subtract(anchor);
193
+ const anchor = this._getAnchor();
194
+ const pos = L.DomUtil.getPosition(this._container).subtract(anchor);
151
195
  L.DomUtil.setPosition(this._container, this._map.rotatedPointToMapPanePoint(pos).add(anchor));
152
196
  } else if (this._map._rotate) {
153
- var mapPanePoint = this._map.rotatedPointToMapPanePoint(this._map.latLngToLayerPoint(this._latlng)), offset = L.point(this.options.offset), anchor = this._getAnchor();
197
+ const mapPanePoint = this._map.rotatedPointToMapPanePoint(this._map.latLngToLayerPoint(this._latlng)), offset = toPoint(this.options.offset || [0, 0]), anchor = this._getAnchor();
154
198
  this._containerLeft = -Math.round(this._containerWidth / 2) + mapPanePoint.x + offset.x + anchor.x;
155
199
  this._containerBottom = -(mapPanePoint.y + offset.y + anchor.y);
156
200
  this._container.style.left = this._containerLeft + "px";
@@ -163,26 +207,33 @@ var leafletRotate = (() => {
163
207
  });
164
208
 
165
209
  // src/layer/Popup.js
166
- var popupProto = L.extend({}, L.Popup.prototype);
210
+ var popupProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Popup.prototype));
167
211
  L.Popup.include({
168
212
  /**
169
- * 0. update element anchor point (popupProto v1.9.3)
170
- * 1. rotate around anchor point (subtract anchor -> rotate point -> add anchor)
213
+ * Update the popup position during zoom animation and transform it into map-pane coordinates.
214
+ *
215
+ * @param {Object} e Zoom animation event.
216
+ *
217
+ * @since leaflet-rotate (v0.1.0)
171
218
  */
172
- _animateZoom: function(e) {
219
+ _animateZoom(e) {
173
220
  popupProto._animateZoom.apply(this, arguments);
174
221
  if (this._map && this._map._rotate) {
175
- var anchor = this._getAnchor();
176
- var pos = L.DomUtil.getPosition(this._container).subtract(anchor);
222
+ const anchor = this._getAnchor();
223
+ const pos = L.DomUtil.getPosition(this._container).subtract(anchor);
177
224
  L.DomUtil.setPosition(this._container, this._map.rotatedPointToMapPanePoint(pos).add(anchor));
178
225
  }
179
226
  },
180
227
  /**
181
- * Fix for L.popup({ keepInView = true })
228
+ * Pan the map when a popup with auto-pan enabled would extend beyond the viewport.
182
229
  *
183
230
  * @see https://github.com/fnicollet/Leaflet/pull/21
231
+ *
232
+ * @fires L.Map#autopanstart
233
+ *
234
+ * @since leaflet-rotate (v0.1.0)
184
235
  */
185
- _adjustPan: function() {
236
+ _adjustPan() {
186
237
  if (!this.options.autoPan || this._map._panAnim && this._map._panAnim._inProgress) {
187
238
  return;
188
239
  }
@@ -190,9 +241,10 @@ var leafletRotate = (() => {
190
241
  this._autopanning = false;
191
242
  return;
192
243
  }
193
- var map = this._map, marginBottom = parseInt(L.DomUtil.getStyle(this._container, "marginBottom"), 10) || 0, containerHeight = this._container.offsetHeight + marginBottom, containerWidth = this._containerWidth, layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
244
+ const map = this._map, marginBottom = parseInt(getComputedStyle(this._container).marginBottom, 10) || 0, containerHeight = this._container.offsetHeight + marginBottom, containerWidth = this._containerWidth, layerPos = new L.Point(this._containerLeft, -containerHeight - this._containerBottom);
194
245
  layerPos._add(L.DomUtil.getPosition(this._container));
195
- var containerPos = layerPos._add(this._map._getMapPanePos()), padding = L.point(this.options.autoPanPadding), paddingTL = L.point(this.options.autoPanPaddingTopLeft || padding), paddingBR = L.point(this.options.autoPanPaddingBottomRight || padding), size = map.getSize(), dx = 0, dy = 0;
246
+ const containerPos = layerPos._add(this._map._getMapPanePos()), padding = toPoint(this.options.autoPanPadding || [0, 0]), paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding), paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding), size = map.getSize();
247
+ let dx = 0, dy = 0;
196
248
  if (containerPos.x + containerWidth + paddingBR.x > size.x) {
197
249
  dx = containerPos.x + containerWidth - size.x + paddingBR.x;
198
250
  }
@@ -215,41 +267,61 @@ var leafletRotate = (() => {
215
267
  });
216
268
 
217
269
  // src/layer/Tooltip.js
218
- var tooltipProto = L.extend({}, L.Tooltip.prototype);
270
+ var tooltipProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Tooltip.prototype));
219
271
  L.Tooltip.include({
220
- _animateZoom: function(e) {
272
+ /**
273
+ * Reposition the tooltip during zoom animation using transformed map coordinates.
274
+ *
275
+ * @param {Object} e Zoom animation event.
276
+ *
277
+ * @since leaflet-rotate (v0.1.0)
278
+ */
279
+ _animateZoom(e) {
221
280
  if (!this._map._rotate) {
222
281
  return tooltipProto._animateZoom.apply(this, arguments);
223
282
  }
224
- var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
283
+ let pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center);
225
284
  pos = this._map.rotatedPointToMapPanePoint(pos);
226
285
  this._setPosition(pos);
227
286
  },
228
- _updatePosition: function() {
287
+ /**
288
+ * Recompute tooltip screen position after map movement.
289
+ *
290
+ * @since leaflet-rotate (v0.1.0)
291
+ */
292
+ _updatePosition() {
229
293
  if (!this._map._rotate) {
230
294
  return tooltipProto._updatePosition.apply(this, arguments);
231
295
  }
232
- var pos = this._map.latLngToLayerPoint(this._latlng);
296
+ let pos = this._map.latLngToLayerPoint(this._latlng);
233
297
  pos = this._map.rotatedPointToMapPanePoint(pos);
234
298
  this._setPosition(pos);
235
299
  }
236
300
  });
237
301
 
238
302
  // src/layer/marker/Icon.js
239
- var iconProto = L.extend({}, L.Icon.prototype);
303
+ var iconProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Icon.prototype));
240
304
  L.Icon.include({
241
- _setIconStyles: function(img, name) {
242
- var options = this.options;
243
- var sizeOption = options[name + "Size"];
305
+ /**
306
+ * Set marker icon dimensions and anchor styles, including the rotation origin.
307
+ *
308
+ * @param {HTMLImageElement} img Icon or shadow element.
309
+ * @param {string} name Icon style key (`icon` or `shadow`).
310
+ *
311
+ * @since leaflet-rotate (v0.1.0)
312
+ */
313
+ _setIconStyles(img, name) {
314
+ const options = this.options;
315
+ let sizeOption = options[name + "Size"];
244
316
  if (typeof sizeOption === "number") {
245
317
  sizeOption = [sizeOption, sizeOption];
246
318
  }
247
- var size = L.point(sizeOption), anchor = L.point(name === "shadow" && options.shadowAnchor || options.iconAnchor || size && size.divideBy(2, true));
319
+ const size = sizeOption ? new L.Point(...Array.isArray(sizeOption) ? sizeOption : [sizeOption.x, sizeOption.y]) : null, anchorOption = name === "shadow" && options.shadowAnchor || options.iconAnchor || size && size.divideBy(2, true), anchor = anchorOption ? new L.Point(...Array.isArray(anchorOption) ? anchorOption : [anchorOption.x, anchorOption.y]) : null;
248
320
  img.className = "leaflet-marker-" + name + " " + (options.className || "");
249
321
  if (anchor) {
250
322
  img.style.marginLeft = -anchor.x + "px";
251
323
  img.style.marginTop = -anchor.y + "px";
252
- img.style[L.DomUtil.TRANSFORM + "Origin"] = anchor.x + "px " + anchor.y + "px 0px";
324
+ img.style.transformOrigin = anchor.x + "px " + anchor.y + "px 0px";
253
325
  }
254
326
  if (size) {
255
327
  img.style.width = size.x + "px";
@@ -259,50 +331,69 @@ var leafletRotate = (() => {
259
331
  });
260
332
 
261
333
  // src/layer/marker/Marker.js
262
- var markerProto = L.extend({}, L.Marker.prototype);
334
+ var markerProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Marker.prototype));
263
335
  L.Marker.mergeOptions({
264
336
  /**
265
- * Rotation of this marker in rad
337
+ * Rotation of the marker icon in radians.
266
338
  *
267
339
  * @type {Number}
340
+ *
341
+ * @since leaflet-rotate (v0.1.0)
268
342
  */
269
343
  rotation: 0,
270
344
  /**
271
- * Rotate this marker when map rotates
345
+ * Rotate the marker icon when the map bearing changes.
346
+ *
347
+ * Combine with `keepUpright: true` to keep the marker readable while
348
+ * preserving its line alignment.
272
349
  *
273
350
  * @type {Boolean}
351
+ *
352
+ * @since leaflet-rotate (v0.1.0)
274
353
  */
275
354
  rotateWithView: false,
276
355
  /**
277
- * Flip a rotated marker when needed to keep it upright
356
+ * Flip the icon when needed to keep it upright while map-aligned.
278
357
  *
279
358
  * @type {Boolean}
359
+ *
360
+ * @since leaflet-rotate (v0.1.0)
280
361
  */
281
362
  keepUpright: false,
282
363
  /**
283
- * Scale of the marker icon
364
+ * Optional scale of the marker icon and shadow.
284
365
  *
285
366
  * @type {Number}
367
+ *
368
+ * @since leaflet-rotate (v0.1.0)
286
369
  */
287
370
  scale: void 0
288
371
  });
289
372
  var markerDragProto;
290
373
  var MarkerDrag = {
291
- // _onDragStart: function() {
374
+ // _onDragStart() {
292
375
  // if (!this._marker._map._rotate) {
293
376
  // return markerDragProto._onDragStart.apply(this, arguments);
294
377
  // }
295
378
  // this._draggable.updateMapBearing(this._marker._map._bearing);
296
379
  // },
297
- _onDrag: function(e) {
298
- var marker = this._marker, rotated_marker = marker.options.rotation || marker.options.rotateWithView, shadow = marker._shadow, iconPos = L.DomUtil.getPosition(marker._icon);
380
+ /**
381
+ * Update a dragged marker's geographic position from its transformed icon position.
382
+ *
383
+ * @param {Object} e Marker drag event with `latlng` and `oldLatLng` fields.
384
+ *
385
+ * @since leaflet-rotate (v0.1.0)
386
+ */
387
+ _onDrag(e) {
388
+ const marker = this._marker, rotated_marker = marker.options.rotation || marker.options.rotateWithView, shadow = marker._shadow;
389
+ let iconPos = L.DomUtil.getPosition(marker._icon);
299
390
  if (!rotated_marker && shadow) {
300
391
  L.DomUtil.setPosition(shadow, iconPos);
301
392
  }
302
393
  if (marker._map._rotate) {
303
394
  iconPos = marker._map.mapPanePointToRotatedPoint(iconPos);
304
395
  }
305
- var latlng = marker._map.layerPointToLatLng(iconPos);
396
+ const latlng = marker._map.layerPointToLatLng(iconPos);
306
397
  marker._latlng = latlng;
307
398
  e.latlng = latlng;
308
399
  e.oldLatLng = this._oldLatLng;
@@ -310,7 +401,14 @@ var leafletRotate = (() => {
310
401
  else marker.fire("move", e);
311
402
  marker.fire("drag", e);
312
403
  },
313
- _onDragEnd: function(e) {
404
+ /**
405
+ * Reposition the marker after a drag ends, then run Leaflet's drag-end logic.
406
+ *
407
+ * @param {Object} e Marker drag-end event.
408
+ *
409
+ * @since leaflet-rotate (v0.1.0)
410
+ */
411
+ _onDragEnd(e) {
314
412
  if (this._marker._map._rotate) {
315
413
  this._marker.update();
316
414
  }
@@ -319,16 +417,44 @@ var leafletRotate = (() => {
319
417
  };
320
418
  L.Marker.include({
321
419
  /**
322
- * Update L.Marker anchor position after the map
323
- * is moved by calling `map.setBearing(theta)`
420
+ * Extend marker event subscriptions to reposition its icon after bearing or pitch changes.
324
421
  *
325
422
  * @listens L.Map~rotate
423
+ * @listens L.Map~pitch
424
+ *
425
+ * @returns {Object} Leaflet event map for the marker.
426
+ *
427
+ * @since leaflet-rotate (v0.1.0)
326
428
  */
327
- getEvents: function() {
328
- return L.extend(markerProto.getEvents.apply(this, arguments), { rotate: this.update });
429
+ getEvents() {
430
+ const events = Object.assign(markerProto.getEvents.apply(this, arguments), {
431
+ rotate: this.update,
432
+ pitch: this._updatePitchEvents
433
+ });
434
+ if (this._map && this._map._pitch) {
435
+ events.move = this.update;
436
+ }
437
+ return events;
438
+ },
439
+ /**
440
+ * Subscribe to map movement while pitch is active to keep the icon aligned.
441
+ *
442
+ * @since leaflet-rotate (0.5.0)
443
+ */
444
+ _updatePitchEvents() {
445
+ if (this._map._pitch) {
446
+ this._map.on("move", this.update, this);
447
+ } else {
448
+ this._map.off("move", this.update, this);
449
+ }
329
450
  },
330
- _initInteraction: function() {
331
- var ret = markerProto._initInteraction.apply(this, arguments);
451
+ /**
452
+ * Install rotation-aware drag callbacks when marker dragging is enabled.
453
+ *
454
+ * @since leaflet-rotate (v0.1.0)
455
+ */
456
+ _initInteraction() {
457
+ const ret = markerProto._initInteraction.apply(this, arguments);
332
458
  if (this.dragging && this.dragging.enabled() && this._map && this._map._rotate) {
333
459
  markerDragProto = markerDragProto || Object.getPrototypeOf(this.dragging);
334
460
  this.dragging.disable();
@@ -341,11 +467,18 @@ var leafletRotate = (() => {
341
467
  }
342
468
  return ret;
343
469
  },
344
- _setPos: function(pos) {
470
+ /**
471
+ * Position and rotate the marker icon and shadow in map-pane coordinates.
472
+ *
473
+ * @param {L.Point} pos Marker anchor in layer-point coordinates.
474
+ *
475
+ * @since leaflet-rotate (v0.1.0)
476
+ */
477
+ _setPos(pos) {
345
478
  if (this._map._rotate) {
346
479
  pos = this._map.rotatedPointToMapPanePoint(pos);
347
480
  }
348
- var bearing = this.options.rotation || 0;
481
+ let bearing = this.options.rotation || 0;
349
482
  if (this.options.rotateWithView) {
350
483
  bearing += this._map._bearing;
351
484
  }
@@ -365,29 +498,41 @@ var leafletRotate = (() => {
365
498
  this._zIndex = pos.y + this.options.zIndexOffset;
366
499
  this._resetZIndex();
367
500
  },
368
- // _updateZIndex: function(offset) {
501
+ // _updateZIndex(offset) {
369
502
  // if (!this._map._rotate) {
370
503
  // return markerProto._updateZIndex.apply(this, arguments);
371
504
  // }
372
505
  // this._icon.style.zIndex = Math.round(this._zIndex + offset);
373
506
  // },
374
- setRotation: function(rotation) {
507
+ /**
508
+ * Set the marker icon's rotation and redraw it.
509
+ *
510
+ * @param {number} rotation Angle in radians.
511
+ *
512
+ * @returns {void}
513
+ *
514
+ * @since leaflet-rotate (v0.1.0)
515
+ */
516
+ setRotation(rotation) {
375
517
  this.options.rotation = rotation;
376
518
  this.update();
377
519
  }
378
520
  });
379
521
 
380
522
  // src/layer/tile/GridLayer.js
381
- var gridLayerProto = L.extend({}, L.GridLayer.prototype);
523
+ var gridLayerProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.GridLayer.prototype));
382
524
  L.GridLayer.include({
383
525
  /**
384
- * Redraw L.TileLayer bounds after the map is
385
- * moved by just calling `map.setBearing(theta)`
526
+ * Add a throttled tile refresh after map bearing changes.
386
527
  *
387
528
  * @listens L.Map~rotate
529
+ *
530
+ * @returns {Object} Leaflet event map for the grid layer.
531
+ *
532
+ * @since leaflet-rotate (v0.1.0)
388
533
  */
389
- getEvents: function() {
390
- var events = gridLayerProto.getEvents.apply(this, arguments);
534
+ getEvents() {
535
+ const events = gridLayerProto.getEvents.apply(this, arguments);
391
536
  if (this._map._rotate && !this.options.updateWhenIdle) {
392
537
  if (!this._onRotate) {
393
538
  this._onRotate = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
@@ -396,7 +541,16 @@ var leafletRotate = (() => {
396
541
  }
397
542
  return events;
398
543
  },
399
- _getTiledPixelBounds: function(center) {
544
+ /**
545
+ * Return tile pixel bounds that cover the rotated viewport.
546
+ *
547
+ * @param {L.LatLng} center View center used to calculate bounds.
548
+ *
549
+ * @returns {L.Bounds} Pixel bounds covered by the layer's tiles.
550
+ *
551
+ * @since leaflet-rotate (v0.1.0)
552
+ */
553
+ _getTiledPixelBounds(center) {
400
554
  if (!this._map._rotate) {
401
555
  return gridLayerProto._getTiledPixelBounds.apply(this, arguments);
402
556
  }
@@ -405,29 +559,58 @@ var leafletRotate = (() => {
405
559
  });
406
560
 
407
561
  // src/layer/vector/Renderer.js
408
- var rendererProto = L.extend({}, L.Renderer.prototype);
562
+ var rendererProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Renderer.prototype));
563
+ var rendererBase = Object.getPrototypeOf(L.Renderer.prototype);
564
+ var rendererGetEvents = rendererProto.getEvents || rendererBase.getEvents;
565
+ var rendererOnMoveEnd = rendererProto._onMoveEnd || rendererBase._onMoveEnd;
409
566
  L.Renderer.include({
410
567
  /**
411
- * Redraw L.Canvas and L.SVG renderer bounds after the
412
- * map is moved by just calling `map.setBearing(theta)`
568
+ * Subscribe the renderer to map rotation so its visible bounds are recalculated.
413
569
  *
414
570
  * @listens L.Map~rotate
571
+ *
572
+ * @returns {Object} Leaflet event map for the renderer.
573
+ *
574
+ * @since leaflet-rotate (v0.1.0)
575
+ */
576
+ getEvents() {
577
+ const events = rendererGetEvents.apply(this, arguments);
578
+ return Object.assign(events, { rotate: this._onRotate });
579
+ },
580
+ /**
581
+ * Reproject vector paths and refresh renderer bounds after bearing changes.
582
+ *
583
+ * @since leaflet-rotate (v0.1.0)
415
584
  */
416
- getEvents: function() {
417
- return L.extend(rendererProto.getEvents.apply(this, arguments), { rotate: this._update });
585
+ _onRotate() {
586
+ if (!this._map._rotate) {
587
+ return;
588
+ }
589
+ this._onZoomEnd();
590
+ if (L.version.startsWith("2.")) {
591
+ this._onMoveEnd();
592
+ } else {
593
+ this._update();
594
+ }
418
595
  },
419
596
  /**
420
- * Fix for `map.flyTo()` when `false === map.options.zoomAnimation`
597
+ * Add the zoom-animation class needed for correct renderer transforms in Leaflet 1.
598
+ *
599
+ * Fixes `map.flyTo()` when `map.options.zoomAnimation` is false.
421
600
  *
422
601
  * @see https://github.com/Leaflet/Leaflet/pull/8794
602
+ *
603
+ * @since leaflet-rotate (v0.2.0)
423
604
  */
424
- onAdd: function() {
605
+ onAdd() {
425
606
  rendererProto.onAdd.apply(this, arguments);
426
607
  if (L.version <= "1.9.3") {
427
608
  this._container.classList.add("leaflet-zoom-animated");
428
609
  }
429
610
  },
430
611
  /**
612
+ * Update the renderer transform during zoom animation, accounting for rotated coordinates.
613
+ *
431
614
  * @FIXME layer drifts on `map.setZoom()` (eg. zoom during animation)
432
615
  *
433
616
  * the main cause seems to be related to `this._updateTransform(path._center, path._zoom))`
@@ -442,20 +625,67 @@ var leafletRotate = (() => {
442
625
  * @see https://github.com/Leaflet/Leaflet/pull/8103
443
626
  * @see https://github.com/Leaflet/Leaflet/issues/7466
444
627
  *
445
- * @TODO rechek this changes from leaflet@v1.9.3
628
+ * @TODO recheck these changes against leaflet@v1.9.3
446
629
  *
447
630
  * @see https://github.com/Leaflet/Leaflet/compare/v1.7.0...v1.9.3
631
+ *
632
+ * @param {L.LatLng} center Target map center.
633
+ * @param {number} zoom Target zoom level.
634
+ *
635
+ * @since leaflet-rotate (v0.1.0)
448
636
  */
449
- _updateTransform: function(center, zoom) {
637
+ _updateTransform(center, zoom) {
638
+ if (L.version.startsWith("2.")) {
639
+ if (this._map._rotate) {
640
+ const scale2 = this._map.getZoomScale(zoom, this._zoom), offset2 = this._topLeftPoint.multiplyBy(scale2).subtract(this._map._getNewPixelOrigin(center, zoom));
641
+ L.DomUtil.setTransform(this._container, offset2, scale2);
642
+ return;
643
+ }
644
+ return rendererBase._updateTransform.apply(this, arguments);
645
+ }
450
646
  if (!this._map._rotate) {
451
647
  return rendererProto._updateTransform.apply(this, arguments);
452
648
  }
453
649
  if (zoom === this._zoom) {
454
650
  return;
455
651
  }
456
- var scale = this._map.getZoomScale(zoom, this._zoom), offset = this._map._latLngToNewLayerPoint(this._topLeft, zoom, center);
652
+ const scale = this._map.getZoomScale(zoom, this._zoom), offset = this._map._latLngToNewLayerPoint(this._topLeft, zoom, center);
457
653
  L.DomUtil.setTransform(this._container, offset, scale);
458
654
  },
655
+ /**
656
+ * Recalculate bounds and finish renderer updates after movement on Leaflet 2.
657
+ *
658
+ * @param {Object} ev Map movement event.
659
+ *
660
+ * @since leaflet-rotate (v0.4.0)
661
+ */
662
+ _onMoveEnd(ev) {
663
+ if (!L.version.startsWith("2.") || !this._map._rotate) {
664
+ return rendererOnMoveEnd.apply(this, arguments);
665
+ }
666
+ this._bounds = this._map._getPaddedPixelBounds(this.options.padding);
667
+ this._topLeft = this._map.layerPointToLatLng(this._bounds.min);
668
+ this._center = this._map.getCenter();
669
+ this._zoom = this._map.getZoom();
670
+ this._topLeftPoint = this._map._getNewPixelOrigin(this._center, this._zoom).add(this._bounds.min);
671
+ this._updateTransform(this._center, this._zoom);
672
+ this._resizeContainer();
673
+ this._onSettled(ev);
674
+ },
675
+ /**
676
+ * Resize the renderer container to cover the pitch-aware map bounds.
677
+ *
678
+ * @since leaflet-rotate (v0.4.0)
679
+ */
680
+ _resizeContainer() {
681
+ if (!L.version.startsWith("2.") || !this._map._rotate || !this._bounds) {
682
+ return rendererBase._resizeContainer.apply(this, arguments);
683
+ }
684
+ const size = this._bounds.getSize();
685
+ this._container.style.width = size.x + "px";
686
+ this._container.style.height = size.y + "px";
687
+ return size;
688
+ },
459
689
  // getEvents() {
460
690
  // const events = {
461
691
  // viewreset: this._reset,
@@ -491,7 +721,12 @@ var leafletRotate = (() => {
491
721
  // this._layers[id]._update();
492
722
  // }
493
723
  // },
494
- _update: function() {
724
+ /**
725
+ * Recalculate renderer bounds and cached view state.
726
+ *
727
+ * @since leaflet-rotate (v0.1.0)
728
+ */
729
+ _update() {
495
730
  if (!this._map._rotate) {
496
731
  return rendererProto._update.apply(this, arguments);
497
732
  }
@@ -503,10 +738,37 @@ var leafletRotate = (() => {
503
738
  });
504
739
 
505
740
  // src/map/Map.js
506
- var mapProto = L.extend({}, L.Map.prototype);
507
- L.Map.mergeOptions({ rotate: false, bearing: 0 });
741
+ var mapProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Map.prototype));
742
+ L.Map.mergeOptions({
743
+ /**
744
+ * @type {boolean} Enable bearing and pitch transformations.
745
+ *
746
+ * @since leaflet-rotate (v0.1.0)
747
+ */
748
+ rotate: false,
749
+ /**
750
+ * @type {number} Initial map bearing in degrees.
751
+ *
752
+ * @since leaflet-rotate (v0.1.0)
753
+ */
754
+ bearing: 0,
755
+ /**
756
+ * @type {number} Initial map pitch in degrees, clamped to 0–85.
757
+ *
758
+ * @since leaflet-rotate (0.5.0)
759
+ */
760
+ pitch: 0
761
+ });
508
762
  L.Map.include({
509
- _animateZoomNoDelay: function(center, zoom) {
763
+ /**
764
+ * Apply an immediate animated-zoom frame without waiting for a transition delay.
765
+ *
766
+ * @param {L.LatLng} center Target center.
767
+ * @param {number} zoom Target zoom level.
768
+ *
769
+ * @since leaflet-rotate (v0.1.0)
770
+ */
771
+ _animateZoomNoDelay(center, zoom) {
510
772
  if (!this._mapPane) {
511
773
  return;
512
774
  }
@@ -517,17 +779,24 @@ var leafletRotate = (() => {
517
779
  this._onZoomTransitionEnd();
518
780
  },
519
781
  /**
520
- * @param {(HTMLElement|String)} id html selector
521
- * @param {Object} [options={}] leaflet map options
782
+ * Initialize the Leaflet map and apply its initial bearing and pitch.
783
+ *
784
+ * @param {HTMLElement|string} id Map container element or its ID.
785
+ * @param {Object} [options={}] Leaflet map options, including `rotate`, `bearing`, and `pitch`.
786
+ *
787
+ * @since leaflet-rotate (v0.1.0)
522
788
  */
523
- initialize: function(id, options) {
789
+ initialize(id, options) {
524
790
  if (options.rotate) {
525
791
  this._rotate = true;
526
792
  this._bearing = 0;
793
+ this._pitch = 0;
527
794
  }
528
795
  mapProto.initialize.apply(this, arguments);
529
796
  if (this.options.rotate) {
797
+ this.on("resize", this._updatePitchTransform, this);
530
798
  this.setBearing(this.options.bearing);
799
+ this.setPitch(this.options.pitch);
531
800
  }
532
801
  },
533
802
  /**
@@ -535,27 +804,42 @@ var leafletRotate = (() => {
535
804
  * returns the corresponding pixel coordinate relative to
536
805
  * the [origin pixel](#map-getpixelorigin).
537
806
  *
538
- * @param {L.Point} point pixel screen coordinates
539
- * @returns {L.Point} transformed pixel point
807
+ * @param {L.Point|number[]} point Pixel coordinate in the map container.
808
+ *
809
+ * @returns {L.Point} Corresponding layer-point coordinate.
810
+ *
811
+ * @since leaflet-rotate (v0.1.0)
540
812
  */
541
- containerPointToLayerPoint: function(point) {
813
+ containerPointToLayerPoint(point) {
542
814
  if (!this._rotate) {
543
815
  return mapProto.containerPointToLayerPoint.apply(this, arguments);
544
816
  }
545
- return L.point(point).subtract(this._getMapPanePos()).rotateFrom(-this._bearing, this._getRotatePanePos()).subtract(this._getRotatePanePos());
817
+ if (this._pitch) {
818
+ return this._transformPitchPoint(
819
+ toPoint(point).subtract(this._getMapPanePos()),
820
+ this._pitchInverseTransform
821
+ );
822
+ }
823
+ return toPoint(point).subtract(this._getMapPanePos()).rotateFrom(-this._bearing, this._getRotatePanePos()).subtract(this._getRotatePanePos());
546
824
  },
547
825
  /**
548
826
  * Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
549
827
  * returns the corresponding pixel coordinate relative to the map container.
550
828
  *
551
- * @param {L.Point} point pixel screen coordinates
552
- * @returns {L.Point} transformed pixel point
829
+ * @param {L.Point|number[]} point Pixel coordinate in layer-point space.
830
+ *
831
+ * @returns {L.Point} Corresponding pixel coordinate in the map container.
832
+ *
833
+ * @since leaflet-rotate (v0.1.0)
553
834
  */
554
- layerPointToContainerPoint: function(point) {
835
+ layerPointToContainerPoint(point) {
555
836
  if (!this._rotate) {
556
837
  return mapProto.layerPointToContainerPoint.apply(this, arguments);
557
838
  }
558
- return L.point(point).add(this._getRotatePanePos()).rotateFrom(this._bearing, this._getRotatePanePos()).add(this._getMapPanePos());
839
+ if (this._pitch) {
840
+ return this._transformPitchPoint(point, this._pitchTransform).add(this._getMapPanePos());
841
+ }
842
+ return toPoint(point).add(this._getRotatePanePos()).rotateFrom(this._bearing, this._getRotatePanePos()).add(this._getMapPanePos());
559
843
  },
560
844
  /**
561
845
  * Converts a coordinate from the rotated pane reference system
@@ -564,13 +848,17 @@ var leafletRotate = (() => {
564
848
  * (rotatePane) --> (mapPane)
565
849
  * (rotatePane) --> (norotatePane)
566
850
  *
567
- * @param {L.Point} point pixel screen coordinates
568
- * @returns {L.Point}
851
+ * @param {L.Point|number[]} point Point in the rotated pane.
569
852
  *
570
- * @since leaflet-rotate (v0.1)
853
+ * @returns {L.Point} Corresponding point in map-pane coordinates.
854
+ *
855
+ * @since leaflet-rotate (v0.1.0)
571
856
  */
572
- rotatedPointToMapPanePoint: function(point) {
573
- return L.point(point).rotate(this._bearing)._add(this._getRotatePanePos());
857
+ rotatedPointToMapPanePoint(point) {
858
+ if (this._pitch) {
859
+ return this._transformPitchPoint(point, this._pitchTransform);
860
+ }
861
+ return toPoint(point).rotate(this._bearing)._add(this._getRotatePanePos());
574
862
  },
575
863
  /**
576
864
  * Converts a coordinate from the not rotated map pane reference system
@@ -579,18 +867,23 @@ var leafletRotate = (() => {
579
867
  * (mapPane) --> (rotatePane)
580
868
  * (norotatePane) --> (rotatePane)
581
869
  *
582
- * @param {L.Point} point pixel screen coordinates
870
+ * @param {L.Point|number[]} point Point in map-pane coordinates.
583
871
  *
584
- * @since leaflet-rotate (v0.1)
872
+ * @returns {L.Point} Corresponding point in the rotated pane.
873
+ *
874
+ * @since leaflet-rotate (v0.1.0)
585
875
  */
586
- mapPanePointToRotatedPoint: function(point) {
587
- return L.point(point)._subtract(this._getRotatePanePos()).rotate(-this._bearing);
876
+ mapPanePointToRotatedPoint(point) {
877
+ if (this._pitch) {
878
+ return this._transformPitchPoint(point, this._pitchInverseTransform);
879
+ }
880
+ return toPoint(point)._subtract(this._getRotatePanePos()).rotate(-this._bearing);
588
881
  },
589
- // latLngToLayerPoint: function (latlng) {
590
- // var projectedPoint = this.project(L.latLng(latlng))._round();
882
+ // latLngToLayerPoint(latlng) {
883
+ // const projectedPoint = this.project(L.latLng(latlng))._round();
591
884
  // return projectedPoint._subtract(this.getPixelOrigin());
592
885
  // },
593
- // latLngToContainerPoint: function (latlng) {
886
+ // latLngToContainerPoint(latlng) {
594
887
  // return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
595
888
  // },
596
889
  /**
@@ -599,39 +892,39 @@ var leafletRotate = (() => {
599
892
  *
600
893
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/map/Map.js#L1114-L1135
601
894
  *
602
- * @param {L.LatLngBounds} bounds
603
- * @returns {L.Bounds}
895
+ * @param {L.LatLngBounds} bounds Geographic bounds to project.
896
+ *
897
+ * @returns {L.Bounds} Axis-aligned bounds in container-pixel coordinates.
604
898
  *
605
- * @since leaflet-rotate (v0.2)
899
+ * @since leaflet-rotate (v0.2.0)
606
900
  */
607
- mapBoundsToContainerBounds: function(bounds) {
901
+ mapBoundsToContainerBounds(bounds) {
608
902
  if (!this._rotate && mapProto.mapBoundsToContainerBounds) {
609
903
  return mapProto.mapBoundsToContainerBounds.apply(this, arguments);
610
904
  }
611
905
  const origin = this.getPixelOrigin();
612
906
  const nw = this.layerPointToContainerPoint(this.project(bounds.getNorthWest())._subtract(origin)), ne = this.layerPointToContainerPoint(this.project(bounds.getNorthEast())._subtract(origin)), sw = this.layerPointToContainerPoint(this.project(bounds.getSouthWest())._subtract(origin)), se = this.layerPointToContainerPoint(this.project(bounds.getSouthEast())._subtract(origin));
613
- return L.bounds([
614
- L.point(Math.min(nw.x, ne.x, se.x, sw.x), Math.min(nw.y, ne.y, se.y, sw.y)),
907
+ return new L.Bounds([
908
+ new L.Point(Math.min(nw.x, ne.x, se.x, sw.x), Math.min(nw.y, ne.y, se.y, sw.y)),
615
909
  // [ minX, minY ]
616
- L.point(Math.max(nw.x, ne.x, se.x, sw.x), Math.max(nw.y, ne.y, se.y, sw.y))
910
+ new L.Point(Math.max(nw.x, ne.x, se.x, sw.x), Math.max(nw.y, ne.y, se.y, sw.y))
617
911
  // [ maxX, maxY ]
618
912
  ]);
619
913
  },
620
914
  /**
621
- * Returns geographical bounds visible in the current map view
915
+ * Return the geographic bounds of the current viewport, accounting for rotation and pitch.
622
916
  *
623
- * @TODO find out if map bounds calculated by `L.Map::getBounds()`
624
- * function should match the `rotatePane` or `norotatePane` bounds
917
+ * The result is the smallest geographic bounds containing all four viewport corners.
625
918
  *
626
- * @see https://github.com/fnicollet/Leaflet/issues/7
919
+ * @returns {L.LatLngBounds} Geographic bounds visible in the map viewport.
627
920
  *
628
- * @returns {L.LatLngBounds}
921
+ * @since leaflet-rotate (v0.1.0)
629
922
  */
630
- getBounds: function() {
923
+ getBounds() {
631
924
  if (!this._rotate) {
632
925
  return mapProto.getBounds.apply(this, arguments);
633
926
  }
634
- var size = this.getSize();
927
+ const size = this.getSize();
635
928
  return new L.LatLngBounds([
636
929
  this.containerPointToLatLng([0, 0]),
637
930
  // topleft
@@ -660,70 +953,199 @@ var leafletRotate = (() => {
660
953
  // return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
661
954
  // },
662
955
  /**
663
- * Change map rotation
956
+ * Set the map bearing, optionally animating to the new value.
957
+ *
958
+ * @param {number} theta Target bearing in degrees; normalized to [0, 360).
959
+ * @param {Object} [options] Animation options.
960
+ * @param {boolean} [options.animate=false] Animate the change.
961
+ * @param {number} [options.duration=0.25] Animation duration in seconds.
962
+ * @param {number} [options.easeLinearity=0.5] Easing curve linearity.
664
963
  *
665
- * @param {number} theta map degrees
666
- * @param {Object} [options] animation options
667
- * @param {boolean} [options.animate=false]
668
- * @param {number} [options.duration=0.25] animation duration in seconds
669
- * @param {number} [options.easeLinearity=0.5]
964
+ * @returns {void}
670
965
  *
671
- * @since leaflet-rotate (v0.1)
966
+ * @fires L.Map#rotate
967
+ *
968
+ * @since leaflet-rotate (v0.1.0)
672
969
  */
673
- setBearing: function(theta, options) {
674
- if (!L.Browser.any3d || !this._rotate) {
970
+ setBearing(theta, options) {
971
+ if (!this._rotate) {
675
972
  return;
676
973
  }
677
974
  if (options && options.animate) {
678
975
  this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
679
- var animationId = this._bearingAnimationId;
976
+ const animationId = this._bearingAnimationId;
680
977
  if (this._bearingAnimationFrame) {
681
- L.Util.cancelAnimFrame(this._bearingAnimationFrame);
978
+ cancelAnimationFrame(this._bearingAnimationFrame);
682
979
  }
683
- var startBearing = this.getBearing(), endBearing = L.Util.wrapNum(theta, [0, 360]), delta = (endBearing - startBearing + 540) % 360 - 180, duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1e3, easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2), startTime = performance.now();
684
- var animate = function() {
980
+ const startBearing = this.getBearing(), endBearing = L.Util.wrapNum(theta, [0, 360]), delta = (endBearing - startBearing + 540) % 360 - 180, duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1e3, easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2), startTime = performance.now();
981
+ const animate = function() {
685
982
  if (!this._mapPane) {
686
983
  delete this._bearingAnimationFrame;
687
984
  return;
688
985
  }
689
- var progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1, eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
986
+ const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1, eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
690
987
  this._setBearing(progress === 1 ? endBearing : startBearing + delta * eased);
691
988
  if (this._bearingAnimationId !== animationId) {
692
989
  return;
693
990
  }
694
991
  if (progress < 1) {
695
- this._bearingAnimationFrame = L.Util.requestAnimFrame(animate, this);
992
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
696
993
  } else {
697
994
  delete this._bearingAnimationFrame;
698
995
  }
699
996
  };
700
- this._bearingAnimationFrame = L.Util.requestAnimFrame(animate, this);
997
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
701
998
  return;
702
999
  }
703
1000
  this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
704
1001
  if (this._bearingAnimationFrame) {
705
- L.Util.cancelAnimFrame(this._bearingAnimationFrame);
1002
+ cancelAnimationFrame(this._bearingAnimationFrame);
706
1003
  delete this._bearingAnimationFrame;
707
1004
  }
708
1005
  this._setBearing(theta);
709
1006
  },
710
- _setBearing: function(theta) {
711
- var bearing = L.Util.wrapNum(theta, [0, 360]) * L.DomUtil.DEG_TO_RAD, center = this._getPixelCenter(), oldPos = this._getRotatePanePos().rotateFrom(-this._bearing, center), newPos = oldPos.rotateFrom(bearing, center);
712
- L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
1007
+ /**
1008
+ * Apply a bearing immediately and update the rotated pane and map coordinates.
1009
+ *
1010
+ * @param {number} theta Bearing in degrees.
1011
+ * @param {L.LatLng} [mapCenter] Center to preserve while applying the change.
1012
+ *
1013
+ * @fires L.Map#rotate
1014
+ *
1015
+ * @since leaflet-rotate (v0.1.0)
1016
+ */
1017
+ _setBearing(theta, mapCenter) {
1018
+ if (!mapCenter && this._loaded) {
1019
+ mapCenter = this.getCenter();
1020
+ }
1021
+ const bearing = L.Util.wrapNum(theta, [0, 360]) * (Math.PI / 180), center = this._getPixelCenter(), oldPos = this._getRotatePanePos().rotateFrom(-this._bearing, center), newPos = oldPos.rotateFrom(bearing, center);
713
1022
  this._pivot = center;
714
1023
  this._bearing = bearing;
715
1024
  this._rotatePanePos = newPos;
1025
+ if (this._pitch) {
1026
+ this._rotatePane._leaflet_pos = oldPos;
1027
+ this._updatePitchTransform();
1028
+ } else {
1029
+ L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
1030
+ }
1031
+ if (mapCenter) {
1032
+ this._pixelOrigin = this.project(mapCenter, this._zoom).subtract(this.containerPointToLayerPoint(this.getSize().divideBy(2)));
1033
+ delete this._lastCenter;
1034
+ this.fire("move");
1035
+ }
716
1036
  this.fire("rotate");
717
1037
  },
718
1038
  /**
719
- * Get current map rotation
1039
+ * Transform a point with a projective matrix, accepting DOMMatrix or its 2D inverse.
1040
+ *
1041
+ * @param {L.Point|number[]} point Input pixel coordinate.
1042
+ * @param {DOMMatrix|Object} matrix Forward or inverse transformation matrix.
1043
+ *
1044
+ * @returns {L.Point} Transformed pixel coordinate.
1045
+ *
1046
+ * @since leaflet-rotate (0.5.0)
1047
+ */
1048
+ _transformPitchPoint(point, matrix) {
1049
+ point = toPoint(point);
1050
+ if (!(matrix instanceof DOMMatrix)) {
1051
+ const denominator = matrix.g * point.x + matrix.h * point.y + matrix.i;
1052
+ return new L.Point(
1053
+ (matrix.a * point.x + matrix.b * point.y + matrix.c) / denominator,
1054
+ (matrix.d * point.x + matrix.e * point.y + matrix.f) / denominator
1055
+ );
1056
+ }
1057
+ const transformed = new DOMPoint(point.x, point.y).matrixTransform(matrix);
1058
+ return new L.Point(transformed.x / transformed.w, transformed.y / transformed.w);
1059
+ },
1060
+ /**
1061
+ * Apply the current pitch/bearing CSS transform and cache its forward and inverse matrices.
1062
+ *
1063
+ * @since leaflet-rotate (0.5.0)
1064
+ */
1065
+ _updatePitchTransform() {
1066
+ if (!this._pitch || !this._rotatePane) {
1067
+ return;
1068
+ }
1069
+ const pivot = this._getPixelCenter(), size = this.getSize(), perspective = Math.max(1, size.y * 1.5), position = this._rotatePane._leaflet_pos || new L.Point(0, 0), transform = `translate3d(${pivot.x}px,${pivot.y}px,0) perspective(${perspective}px) rotateX(${this.getPitch()}deg) rotateZ(${this.getBearing()}deg) translate3d(${position.x - pivot.x}px,${position.y - pivot.y}px,0)`;
1070
+ this._rotatePane.style.transformOrigin = "0 0";
1071
+ this._rotatePane.style.transform = transform;
1072
+ const matrix = this._pitchTransform = new DOMMatrix(getComputedStyle(this._rotatePane).transform), a = matrix.m11, b = matrix.m21, c = matrix.m41, d = matrix.m12, e = matrix.m22, f = matrix.m42, g = matrix.m14, h = matrix.m24, i = matrix.m44, determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
1073
+ this._pitchInverseTransform = {
1074
+ a: (e * i - f * h) / determinant,
1075
+ b: (c * h - b * i) / determinant,
1076
+ c: (b * f - c * e) / determinant,
1077
+ d: (f * g - d * i) / determinant,
1078
+ e: (a * i - c * g) / determinant,
1079
+ f: (c * d - a * f) / determinant,
1080
+ g: (d * h - e * g) / determinant,
1081
+ h: (b * g - a * h) / determinant,
1082
+ i: (a * e - b * d) / determinant
1083
+ };
1084
+ },
1085
+ /**
1086
+ * Set the map pitch in degrees. Values are clamped to the range 0–85.
1087
+ *
1088
+ * @param {number} pitch Pitch angle in degrees; clamped to [0, 85].
1089
+ *
1090
+ * @returns {L.Map} This map instance.
1091
+ *
1092
+ * @fires L.Map#pitch
1093
+ *
1094
+ * @since leaflet-rotate (0.5.0)
1095
+ */
1096
+ setPitch(pitch) {
1097
+ this._setPitchAndBearing(pitch, this.getBearing());
1098
+ return this;
1099
+ },
1100
+ /**
1101
+ * Update pitch and bearing together, preserving the current map center.
1102
+ * Used by the Ctrl+drag handler to avoid applying two separate transforms.
1103
+ *
1104
+ * @param {number} pitch Pitch angle in degrees.
1105
+ * @param {number} bearing Bearing angle in degrees.
1106
+ *
1107
+ * @fires L.Map#pitch
1108
+ * @fires L.Map#rotate
1109
+ *
1110
+ * @since leaflet-rotate (0.5.0)
1111
+ */
1112
+ _setPitchAndBearing(pitch, bearing) {
1113
+ if (!this._rotate) {
1114
+ return;
1115
+ }
1116
+ const clampedPitch = Math.max(0, Math.min(pitch, 85)) * (Math.PI / 180), clampedBearing = L.Util.wrapNum(bearing, [0, 360]) * (Math.PI / 180), pitchChanged = clampedPitch !== this._pitch;
1117
+ if (!pitchChanged && clampedBearing === this._bearing) {
1118
+ return;
1119
+ }
1120
+ const mapCenter = this._loaded ? this.getCenter() : void 0;
1121
+ if (pitchChanged) {
1122
+ this._pitch = clampedPitch;
1123
+ this._rotatePane.style.transformOrigin = L.version.startsWith("2.") ? "0 0" : "";
1124
+ }
1125
+ this._setBearing(bearing, mapCenter);
1126
+ if (pitchChanged) {
1127
+ this.fire("pitch");
1128
+ }
1129
+ },
1130
+ /**
1131
+ * Get the current map pitch in degrees.
720
1132
  *
721
- * @returns {number} theta map degrees
1133
+ * @returns {number} Pitch angle in degrees.
722
1134
  *
723
- * @since leaflet-rotate (v0.1)
1135
+ * @since leaflet-rotate (0.5.0)
724
1136
  */
725
- getBearing: function() {
726
- return this._bearing * L.DomUtil.RAD_TO_DEG;
1137
+ getPitch() {
1138
+ return (this._pitch || 0) * (180 / Math.PI);
1139
+ },
1140
+ /**
1141
+ * Get the current map bearing. Enable the `rotate` map option before calling.
1142
+ *
1143
+ * @returns {number} Bearing in degrees, normalized to [0, 360).
1144
+ *
1145
+ * @since leaflet-rotate (v0.1.0)
1146
+ */
1147
+ getBearing() {
1148
+ return this._bearing * (180 / Math.PI);
727
1149
  },
728
1150
  /**
729
1151
  * Creates a new [map pane](#map-pane) with the given name if it doesn't
@@ -732,9 +1154,12 @@ var leafletRotate = (() => {
732
1154
  *
733
1155
  * @param {String} name leaflet pane
734
1156
  * @param {HTMLElement} [container] parent element
1157
+ *
735
1158
  * @returns {HTMLElement} pane container
1159
+ *
1160
+ * @since leaflet-rotate (v0.1.0)
736
1161
  */
737
- // createPane: function(name, container) {
1162
+ // createPane(name, container) {
738
1163
  // if (!this._rotate || name == 'mapPane') {
739
1164
  // return mapProto.createPane.apply(this, arguments);
740
1165
  // }
@@ -762,14 +1187,23 @@ var leafletRotate = (() => {
762
1187
  * - tooltipPane [HTMLElement = 650] - Pane for tooltips.
763
1188
  * - popupPane [HTMLElement = 700] - Pane for popups.
764
1189
  */
765
- _initPanes: function() {
766
- var panes = this._panes = {};
1190
+ /**
1191
+ * Create the map's panes, placing tile/vector layers in the transformed pane
1192
+ * and markers/overlays in the pane that remains upright.
1193
+ *
1194
+ * @since leaflet-rotate (v0.1.0)
1195
+ */
1196
+ _initPanes() {
1197
+ const panes = this._panes = {};
767
1198
  this._paneRenderers = {};
768
1199
  this._mapPane = this.createPane("mapPane", this._container);
769
1200
  L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
770
1201
  if (this._rotate) {
771
1202
  this._rotatePane = this.createPane("rotatePane", this._mapPane);
772
1203
  this._norotatePane = this.createPane("norotatePane", this._mapPane);
1204
+ if (L.version.startsWith("2.")) {
1205
+ this._rotatePane.style.transformOrigin = "0 0";
1206
+ }
773
1207
  this.createPane("tilePane", this._rotatePane);
774
1208
  this.createPane("overlayPane", this._rotatePane);
775
1209
  this.createPane("shadowPane", this._norotatePane);
@@ -785,8 +1219,8 @@ var leafletRotate = (() => {
785
1219
  this.createPane("popupPane");
786
1220
  }
787
1221
  if (!this.options.markerZoomAnimation) {
788
- L.DomUtil.addClass(panes.markerPane, "leaflet-zoom-hide");
789
- L.DomUtil.addClass(panes.shadowPane, "leaflet-zoom-hide");
1222
+ panes.markerPane.classList.add("leaflet-zoom-hide");
1223
+ panes.shadowPane.classList.add("leaflet-zoom-hide");
790
1224
  }
791
1225
  },
792
1226
  /**
@@ -797,17 +1231,19 @@ var leafletRotate = (() => {
797
1231
  *
798
1232
  * @see https://github.com/Raruto/leaflet-rotate/issues/18
799
1233
  *
800
- * @param {L.LatLng} latlng coordinates
801
- * @param {Object} [options={}] padding options
1234
+ * @param {L.LatLng} latlng Geographic point to keep visible.
1235
+ * @param {Object} [options={}] Padding options (`padding`, `paddingTopLeft`, `paddingBottomRight`).
802
1236
  *
803
1237
  * @returns {L.Map} current map instance
1238
+ *
1239
+ * @since leaflet-rotate (v0.1.4)
804
1240
  */
805
1241
  panInside(latlng, options) {
806
- if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
1242
+ if (!this._rotate || !this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1) {
807
1243
  return mapProto.panInside.apply(this, arguments);
808
1244
  }
809
1245
  options = options || {};
810
- const paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]), pixelPoint = this.latLngToContainerPoint(latlng), pixelBounds = L.bounds([L.point(0, 0), this.getSize()]), pixelCenter = pixelBounds.getCenter(), paddedBounds = L.bounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]), paddedSize = paddedBounds.getSize();
1246
+ const paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]), paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]), pixelPoint = this.latLngToContainerPoint(latlng), pixelBounds = new L.Bounds([new L.Point(0, 0), this.getSize()]), pixelCenter = pixelBounds.getCenter(), paddedBounds = new L.Bounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]), paddedSize = paddedBounds.getSize();
811
1247
  if (!paddedBounds.contains(pixelPoint)) {
812
1248
  this._enforcingBounds = true;
813
1249
  const centerOffset = pixelPoint.subtract(paddedBounds.getCenter());
@@ -829,13 +1265,16 @@ var leafletRotate = (() => {
829
1265
  *
830
1266
  * @see https://github.com/fnicollet/Leaflet/issues/7
831
1267
  *
832
- * @param {L.LatLngBounds} bounds coordinates
833
- * @param {Object} [options] pan options
834
- * @returns {L.Map} current map instance
1268
+ * @param {L.LatLngBounds} bounds Geographic bounds to fit inside the viewport.
1269
+ * @param {Object} [options] Pan animation options.
1270
+ *
1271
+ * @returns {L.Map} This map instance.
1272
+ *
1273
+ * @since leaflet-rotate (v0.1.0)
835
1274
  */
836
- // panInsideBounds: function (bounds, options) {
1275
+ // panInsideBounds(bounds, options) {
837
1276
  // this._enforcingBounds = true;
838
- // var center = this.getCenter(),
1277
+ // const center = this.getCenter(),
839
1278
  // newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds));
840
1279
  //
841
1280
  // if (!center.equals(newCenter)) {
@@ -896,18 +1335,20 @@ var leafletRotate = (() => {
896
1335
  * instead returns the minimum zoom level on which the map view fits into
897
1336
  * the given bounds in its entirety.
898
1337
  *
899
- * @param {L.LatLngBounds} bounds
900
- * @param {Boolean} [inside=false]
901
- * @param {L.Point} [padding=[0,0]]
1338
+ * @param {L.LatLngBounds} bounds Geographic bounds to fit.
1339
+ * @param {boolean} [inside=false] Fit the viewport inside bounds instead of bounds inside the viewport.
1340
+ * @param {L.Point|number[]} [padding=[0,0]] Pixel padding around the viewport.
1341
+ *
1342
+ * @returns {number} Zoom level satisfying the fit.
902
1343
  *
903
- * @returns {Number} zoom level
1344
+ * @since leaflet-rotate (v0.1.0)
904
1345
  */
905
1346
  getBoundsZoom(bounds, inside, padding) {
906
- if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
1347
+ if (!this._rotate || !this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1) {
907
1348
  return mapProto.getBoundsZoom.apply(this, arguments);
908
1349
  }
909
- bounds = L.latLngBounds(bounds);
910
- padding = L.point(padding || [0, 0]);
1350
+ bounds = bounds instanceof L.LatLngBounds ? bounds : new L.LatLngBounds(bounds);
1351
+ padding = toPoint(padding || [0, 0]);
911
1352
  let zoom = this.getZoom() || 0;
912
1353
  const min = this.getMinZoom(), max = this.getMaxZoom(), size = this.getSize().subtract(padding), boundsSize = this.mapBoundsToContainerBounds(bounds).getSize(), snap = this.options.zoomSnap, scalex = size.x / boundsSize.x, scaley = size.y / boundsSize.y, scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);
913
1354
  zoom = this.getScaleZoom(scale, zoom);
@@ -922,50 +1363,78 @@ var leafletRotate = (() => {
922
1363
  *
923
1364
  * @returns {L.Point} layer center
924
1365
  */
925
- // _getCenterLayerPoint: function () {
1366
+ // _getCenterLayerPoint() {
926
1367
  // return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
927
1368
  // },
928
1369
  /**
929
- * Offset of the specified place to the current center in pixels
1370
+ * Return the pixel offset from the current center to the given geographic point.
930
1371
  *
931
- * @param {L.LatLng} latlng map coordinates
1372
+ * @param {L.LatLng} latlng Geographic point.
1373
+ *
1374
+ * @returns {L.Point} Offset in layer pixels, adjusted for bearing and pitch.
1375
+ *
1376
+ * @since leaflet-rotate (v0.1.0)
932
1377
  */
933
- _getCenterOffset: function(latlng) {
934
- var centerOffset = mapProto._getCenterOffset.apply(this, arguments);
935
- if (this._rotate) {
1378
+ _getCenterOffset(latlng) {
1379
+ let centerOffset = mapProto._getCenterOffset.apply(this, arguments);
1380
+ if (this._pitch) {
1381
+ const center = this.getSize()._divideBy(2);
1382
+ centerOffset = this.containerPointToLayerPoint(center.add(centerOffset)).subtract(this.containerPointToLayerPoint(center));
1383
+ } else if (this._rotate) {
936
1384
  centerOffset = centerOffset.rotate(this._bearing);
937
1385
  }
938
1386
  return centerOffset;
939
1387
  },
940
1388
  /**
941
- * @since leaflet-rotate (v0.1)
1389
+ * Return the current position of the rotated pane.
1390
+ *
1391
+ * @returns {L.Point} Rotated-pane position in map-pane coordinates.
1392
+ *
1393
+ * @since leaflet-rotate (v0.1.0)
942
1394
  */
943
- _getRotatePanePos: function() {
1395
+ _getRotatePanePos() {
944
1396
  return this._rotatePanePos || new L.Point(0, 0);
945
1397
  },
946
1398
  // _latLngToNewLayerPoint(latlng, zoom, center) {
947
1399
  // const topLeft = this._getNewPixelOrigin(center, zoom);
948
1400
  // return this.project(latlng, zoom)._subtract(topLeft);
949
1401
  //},
950
- _getNewPixelOrigin: function(center, zoom) {
951
- if (!this._rotate) {
1402
+ /**
1403
+ * Calculate the pixel origin for a target view while preserving bearing-aware alignment.
1404
+ *
1405
+ * @param {L.LatLng} center Target center.
1406
+ * @param {number} zoom Target zoom level.
1407
+ *
1408
+ * @returns {L.Point} New pixel origin.
1409
+ *
1410
+ * @since leaflet-rotate (v0.1.0)
1411
+ */
1412
+ _getNewPixelOrigin(center, zoom) {
1413
+ if (!this._rotate || L.version.startsWith("2.")) {
952
1414
  return mapProto._getNewPixelOrigin.apply(this, arguments);
953
1415
  }
954
- var viewHalf = this.getSize()._divideBy(2);
1416
+ const viewHalf = this.getSize()._divideBy(2);
955
1417
  return this.project(center, zoom).rotate(this._bearing)._subtract(viewHalf)._add(this._getMapPanePos())._add(this._getRotatePanePos()).rotate(-this._bearing)._round();
956
1418
  },
957
1419
  /**
958
- * @since leaflet-rotate (v0.2)
1420
+ * Calculate the pixel bounds needed to cover the transformed viewport.
1421
+ *
1422
+ * @param {L.LatLng} [center=this.getCenter()] Target center.
1423
+ * @param {number} [zoom=this.getZoom()] Target zoom level.
1424
+ *
1425
+ * @returns {L.Bounds} Pixel bounds in projected map coordinates.
1426
+ *
1427
+ * @since leaflet-rotate (v0.2.0)
959
1428
  *
960
1429
  * @see src\layer\tile\GridLayer::_getTiledPixelBounds()
961
1430
  */
962
- _getNewPixelBounds: function(center, zoom) {
1431
+ _getNewPixelBounds(center, zoom) {
963
1432
  center = center || this.getCenter();
964
1433
  zoom = zoom || this.getZoom();
965
1434
  if (!this._rotate && mapProto._getNewPixelBounds) {
966
1435
  return mapProto._getNewPixelBounds.apply(this, arguments);
967
1436
  }
968
- var mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(), scale = this.getZoomScale(mapZoom, zoom), pixelCenter = this.project(center, zoom).floor(), size = this.getSize(), halfSize = new L.Bounds([
1437
+ const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(), scale = this.getZoomScale(mapZoom, zoom), pixelCenter = this.project(center, zoom).floor(), size = this.getSize(), halfSize = new L.Bounds([
969
1438
  this.containerPointToLayerPoint([0, 0]).floor(),
970
1439
  this.containerPointToLayerPoint([size.x, 0]).floor(),
971
1440
  this.containerPointToLayerPoint([0, size.y]).floor(),
@@ -974,26 +1443,30 @@ var leafletRotate = (() => {
974
1443
  return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
975
1444
  },
976
1445
  /**
977
- * @since leaflet-rotate (v0.2)
1446
+ * @since leaflet-rotate (v0.2.0)
978
1447
  *
979
- * @return {L.Point} map pivot point (center)
1448
+ * @returns {L.Point} Map pivot point in pixel coordinates.
980
1449
  */
981
- _getPixelCenter: function() {
1450
+ _getPixelCenter() {
982
1451
  if (!this._rotate && mapProto._getPixelCenter) {
983
1452
  return mapProto._getPixelCenter.apply(this, arguments);
984
1453
  }
985
1454
  return this.getSize()._divideBy(2)._subtract(this._getMapPanePos());
986
1455
  },
987
1456
  /**
988
- * @since leaflet-rotate (v0.2)
989
- *
990
1457
  * @see src\layer\vector\Renderer::_update()
1458
+ *
1459
+ * @param {number} padding Bounds padding as a fraction of map size.
1460
+ *
1461
+ * @returns {L.Bounds} Padded bounds in layer-point coordinates.
1462
+ *
1463
+ * @since leaflet-rotate (v0.2.0)
991
1464
  */
992
- _getPaddedPixelBounds: function(padding) {
1465
+ _getPaddedPixelBounds(padding) {
993
1466
  if (!this._rotate && mapProto._getPaddedPixelBounds) {
994
1467
  return mapProto._getPaddedPixelBounds.apply(this, arguments);
995
1468
  }
996
- var p = padding, size = this.getSize(), padMin = size.multiplyBy(-p), padMax = size.multiplyBy(1 + p);
1469
+ const p = padding, size = this.getSize(), padMin = size.multiplyBy(-p), padMax = size.multiplyBy(1 + p);
997
1470
  return new L.Bounds([
998
1471
  this.containerPointToLayerPoint([padMin.x, padMin.y]).floor(),
999
1472
  this.containerPointToLayerPoint([padMin.x, padMax.y]).floor(),
@@ -1001,23 +1474,32 @@ var leafletRotate = (() => {
1001
1474
  this.containerPointToLayerPoint([padMax.x, padMax.y]).floor()
1002
1475
  ]);
1003
1476
  },
1004
- _handleGeolocationResponse: function(pos) {
1477
+ /**
1478
+ * Build and fire Leaflet's location-found event, including the device heading.
1479
+ *
1480
+ * @param {GeolocationPosition} pos Browser geolocation result.
1481
+ *
1482
+ * @fires L.Map#locationfound
1483
+ *
1484
+ * @since leaflet-rotate (v0.1.0)
1485
+ */
1486
+ _handleGeolocationResponse(pos) {
1005
1487
  if (!this._container._leaflet_id) {
1006
1488
  return;
1007
1489
  }
1008
- var lat = pos.coords.latitude, lng = pos.coords.longitude, hdg = pos.coords.heading, latlng = new L.LatLng(lat, lng), bounds = latlng.toBounds(pos.coords.accuracy), options = this._locateOptions;
1490
+ const lat = pos.coords.latitude, lng = pos.coords.longitude, hdg = pos.coords.heading, latlng = new L.LatLng(lat, lng), bounds = latlng.toBounds(pos.coords.accuracy), options = this._locateOptions;
1009
1491
  if (options.setView) {
1010
- var zoom = this.getBoundsZoom(bounds);
1492
+ const zoom = this.getBoundsZoom(bounds);
1011
1493
  this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
1012
1494
  }
1013
- var data = {
1495
+ const data = {
1014
1496
  latlng,
1015
1497
  bounds,
1016
1498
  timestamp: pos.timestamp,
1017
1499
  /** @TODO use mapProto._handleGeolocationResponse */
1018
1500
  heading: hdg
1019
1501
  };
1020
- for (var i in pos.coords) {
1502
+ for (const i in pos.coords) {
1021
1503
  if (typeof pos.coords[i] === "number") {
1022
1504
  data[i] = pos.coords[i];
1023
1505
  }
@@ -1028,10 +1510,11 @@ var leafletRotate = (() => {
1028
1510
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/geo/LatLngBounds.js#L253-L264
1029
1511
  *
1030
1512
  * @param {L.Bounds} points
1513
+ *
1031
1514
  * @returns {L.Bounds}
1032
1515
  */
1033
1516
  // toCircumscribedBounds(points) {
1034
- // var minX = points.reduce(function (pv, v) { return Math.min(pv, v.x); }, points[0].x),
1517
+ // const minX = points.reduce(function (pv, v) { return Math.min(pv, v.x); }, points[0].x),
1035
1518
  // maxX = points.reduce(function (pv, v) { return Math.max(pv, v.x); }, points[0].x),
1036
1519
  // minY = points.reduce(function (pv, v) { return Math.min(pv, v.y); }, points[0].y),
1037
1520
  // maxY = points.reduce(function (pv, v) { return Math.max(pv, v.y); }, points[0].y);
@@ -1042,7 +1525,12 @@ var leafletRotate = (() => {
1042
1525
 
1043
1526
  // src/map/handler/CompassBearing.js
1044
1527
  L.Map.CompassBearing = L.Handler.extend({
1045
- initialize: function(map) {
1528
+ /**
1529
+ * @param {L.Map} map Map whose bearing follows device orientation.
1530
+ *
1531
+ * @since leaflet-rotate (v0.1.0)
1532
+ */
1533
+ initialize(map) {
1046
1534
  this._map = map;
1047
1535
  if ("ondeviceorientationabsolute" in window) {
1048
1536
  this.__deviceOrientationEvent = "deviceorientationabsolute";
@@ -1051,19 +1539,31 @@ var leafletRotate = (() => {
1051
1539
  }
1052
1540
  this._throttled = L.Util.throttle(this._onDeviceOrientation, 100, this);
1053
1541
  },
1054
- addHooks: function() {
1542
+ /**
1543
+ * Subscribe to the supported device-orientation event, if available.
1544
+ *
1545
+ * @since leaflet-rotate (v0.1.0)
1546
+ */
1547
+ addHooks() {
1055
1548
  if (this._map._rotate && this.__deviceOrientationEvent) {
1056
1549
  L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this);
1057
1550
  } else {
1058
1551
  this.disable();
1059
1552
  }
1060
1553
  },
1061
- removeHooks: function() {
1554
+ /**
1555
+ * Unsubscribe from device orientation when the handler is disabled.
1556
+ *
1557
+ * @since leaflet-rotate (v0.1.0)
1558
+ */
1559
+ removeHooks() {
1062
1560
  if (this._map._rotate && this.__deviceOrientationEvent) {
1063
1561
  L.DomEvent.off(window, this.__deviceOrientationEvent, this._throttled, this);
1064
1562
  }
1065
1563
  },
1066
1564
  /**
1565
+ * Apply a device orientation reading to the map bearing.
1566
+ *
1067
1567
  * `DeviceOrientationEvent.absolute` - Indicates whether the device is providing absolute
1068
1568
  * orientation values (relatives to Magnetic North) or
1069
1569
  * using some arbitrary frame determined by the device.
@@ -1080,10 +1580,14 @@ var leafletRotate = (() => {
1080
1580
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/absolute
1081
1581
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/alpha
1082
1582
  * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/orientation
1583
+ *
1584
+ * @param {DeviceOrientationEvent} e Orientation reading from the browser.
1585
+ *
1586
+ * @since leaflet-rotate (v0.1.0)
1083
1587
  */
1084
- _onDeviceOrientation: function(e) {
1085
- var angle = e.webkitCompassHeading != null ? e.webkitCompassHeading : e.alpha;
1086
- var deviceOrientation = 0;
1588
+ _onDeviceOrientation(e) {
1589
+ let angle = e.webkitCompassHeading != null ? e.webkitCompassHeading : e.alpha;
1590
+ let deviceOrientation = 0;
1087
1591
  if (!Number.isFinite(angle)) {
1088
1592
  return;
1089
1593
  }
@@ -1108,12 +1612,19 @@ var leafletRotate = (() => {
1108
1612
  *
1109
1613
  * @type {Boolean}
1110
1614
  *
1615
+ * @since leaflet-rotate (v0.1.0)
1616
+ *
1111
1617
  * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
1112
1618
  */
1113
1619
  trackContainerMutation: false
1114
1620
  });
1115
1621
  L.Map.ContainerMutation = L.Handler.extend({
1116
- addHooks: function() {
1622
+ /**
1623
+ * Start observing the map container's style attribute.
1624
+ *
1625
+ * @since leaflet-rotate (v0.1.0)
1626
+ */
1627
+ addHooks() {
1117
1628
  if (!this._observer) {
1118
1629
  this._observer = new MutationObserver(L.Util.bind(this._map.invalidateSize, this._map));
1119
1630
  }
@@ -1125,41 +1636,77 @@ var leafletRotate = (() => {
1125
1636
  attributeFilter: ["style"]
1126
1637
  });
1127
1638
  },
1128
- removeHooks: function() {
1639
+ /**
1640
+ * Stop observing container mutations.
1641
+ *
1642
+ * @since leaflet-rotate (v0.1.0)
1643
+ */
1644
+ removeHooks() {
1129
1645
  this._observer.disconnect();
1130
1646
  }
1131
1647
  });
1132
1648
  L.Map.addInitHook("addHandler", "trackContainerMutation", L.Map.ContainerMutation);
1133
1649
 
1134
1650
  // src/map/handler/TouchGestures.js
1651
+ function touchToContainerPoint(map, touch) {
1652
+ const container = map.getContainer(), rect = container.getBoundingClientRect();
1653
+ return new L.Point(
1654
+ touch.clientX - rect.left - container.clientLeft,
1655
+ touch.clientY - rect.top - container.clientTop
1656
+ );
1657
+ }
1135
1658
  L.Map.mergeOptions({
1136
1659
  /**
1137
- * Set it to false if you don't want the map to
1138
- * zoom beyond min/max zoom and then bounce back
1139
- * when pinch-zooming.
1660
+ * Whether pinch zoom may temporarily exceed min/max zoom and bounce back.
1140
1661
  *
1141
1662
  * @type {Boolean}
1663
+ *
1664
+ * @since leaflet-rotate (v0.1.0)
1142
1665
  */
1143
1666
  bounceAtZoomLimits: true
1144
1667
  });
1145
1668
  L.Map.TouchGestures = L.Handler.extend({
1146
- initialize: function(map) {
1669
+ /**
1670
+ * Initialize the shared handler from the map's touch zoom/rotation options.
1671
+ *
1672
+ * @param {L.Map} map Map whose touch gestures are handled.
1673
+ *
1674
+ * @since leaflet-rotate (v0.1.0)
1675
+ */
1676
+ initialize(map) {
1147
1677
  this._map = map;
1148
1678
  this.rotate = !!this._map.options.touchRotate;
1149
1679
  this.zoom = !!this._map.options.touchZoom;
1150
1680
  },
1151
- addHooks: function() {
1681
+ /**
1682
+ * Listen for the start of two-finger gestures on the map container.
1683
+ *
1684
+ * @since leaflet-rotate (v0.1.0)
1685
+ */
1686
+ addHooks() {
1152
1687
  L.DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this);
1153
1688
  },
1154
- removeHooks: function() {
1689
+ /**
1690
+ * Remove the touch-start listener.
1691
+ *
1692
+ * @since leaflet-rotate (v0.1.0)
1693
+ */
1694
+ removeHooks() {
1155
1695
  L.DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this);
1156
1696
  },
1157
- _onTouchStart: function(e) {
1158
- var map = this._map;
1697
+ /**
1698
+ * Record initial touch points and start tracking a two-finger gesture.
1699
+ *
1700
+ * @param {TouchEvent} e Touch-start event.
1701
+ *
1702
+ * @since leaflet-rotate (v0.1.0)
1703
+ */
1704
+ _onTouchStart(e) {
1705
+ const map = this._map;
1159
1706
  if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating) {
1160
1707
  return;
1161
1708
  }
1162
- var p1 = map.mouseEventToContainerPoint(e.touches[0]), p2 = map.mouseEventToContainerPoint(e.touches[1]), vector = p1.subtract(p2);
1709
+ const p1 = touchToContainerPoint(map, e.touches[0]), p2 = touchToContainerPoint(map, e.touches[1]), vector = p1.subtract(p2);
1163
1710
  this._centerPoint = map.getSize()._divideBy(2);
1164
1711
  this._startLatLng = map.containerPointToLatLng(this._centerPoint);
1165
1712
  if (this.zoom) {
@@ -1187,14 +1734,22 @@ var leafletRotate = (() => {
1187
1734
  L.DomEvent.on(document, "touchmove", this._onTouchMove, this).on(document, "touchend touchcancel", this._onTouchEnd, this);
1188
1735
  L.DomEvent.preventDefault(e);
1189
1736
  },
1190
- _onTouchMove: function(e) {
1737
+ /**
1738
+ * Update bearing and/or zoom while the two touch points move.
1739
+ *
1740
+ * @param {TouchEvent} e Touch-move event.
1741
+ *
1742
+ * @since leaflet-rotate (v0.1.0)
1743
+ */
1744
+ _onTouchMove(e) {
1191
1745
  if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) {
1192
1746
  return;
1193
1747
  }
1194
- var map = this._map, p1 = map.mouseEventToContainerPoint(e.touches[0]), p2 = map.mouseEventToContainerPoint(e.touches[1]), vector = p1.subtract(p2), scale = p1.distanceTo(p2) / this._startDist, delta;
1748
+ const map = this._map, p1 = touchToContainerPoint(map, e.touches[0]), p2 = touchToContainerPoint(map, e.touches[1]), vector = p1.subtract(p2), scale = p1.distanceTo(p2) / this._startDist;
1749
+ let delta;
1195
1750
  if (this._rotating) {
1196
- var theta = Math.atan(vector.x / vector.y);
1197
- var bearingDelta = (theta - this._startTheta) * L.DomUtil.RAD_TO_DEG;
1751
+ const theta = Math.atan(vector.x / vector.y);
1752
+ let bearingDelta = (theta - this._startTheta) * (180 / Math.PI);
1198
1753
  if (vector.y < 0) {
1199
1754
  bearingDelta += 180;
1200
1755
  }
@@ -1217,17 +1772,16 @@ var leafletRotate = (() => {
1217
1772
  if (scale === 1 && delta.x === 0 && delta.y === 0) {
1218
1773
  return;
1219
1774
  }
1220
- var alpha = -map.getBearing() * L.DomUtil.DEG_TO_RAD;
1221
- this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(delta.rotate(alpha)));
1775
+ const mapDelta = map._pitch ? map.containerPointToLayerPoint(this._centerPoint.add(delta)).subtract(map.containerPointToLayerPoint(this._centerPoint)) : delta.rotate(-map.getBearing() * (Math.PI / 180));
1776
+ this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(mapDelta));
1222
1777
  }
1223
1778
  }
1224
1779
  if (!this._moved) {
1225
1780
  map._moveStart(true, false);
1226
1781
  this._moved = true;
1227
1782
  }
1228
- L.Util.cancelAnimFrame(this._animRequest);
1229
- var moveFn = map._move.bind(map, this._center, this._zoom, { pinch: true, round: false }, void 0);
1230
- this._animRequest = L.Util.requestAnimFrame(moveFn, this, true);
1783
+ const moveFn = map._move.bind(map, this._center, this._zoom, { pinch: true, round: false }, void 0);
1784
+ moveFn();
1231
1785
  if (this.zoom) {
1232
1786
  if (map.options.zoomAnimation) {
1233
1787
  map._animateZoomNoDelay(this._center, map._limitZoom(this._zoom));
@@ -1237,14 +1791,18 @@ var leafletRotate = (() => {
1237
1791
  }
1238
1792
  L.DomEvent.preventDefault(e);
1239
1793
  },
1240
- _onTouchEnd: function() {
1794
+ /**
1795
+ * Finish the gesture, settle the zoom and remove document listeners.
1796
+ *
1797
+ * @since leaflet-rotate (v0.1.0)
1798
+ */
1799
+ _onTouchEnd() {
1241
1800
  if (!this._moved || !(this._zooming || this._rotating)) {
1242
1801
  this._zooming = false;
1243
1802
  return;
1244
1803
  }
1245
1804
  this._zooming = false;
1246
1805
  this._rotating = false;
1247
- L.Util.cancelAnimFrame(this._animRequest);
1248
1806
  L.DomEvent.off(document, "touchmove", this._onTouchMove, this).off(document, "touchend touchcancel", this._onTouchEnd, this);
1249
1807
  if (this.zoom) {
1250
1808
  if (this._map.options.zoomAnimation) {
@@ -1263,15 +1821,27 @@ var leafletRotate = (() => {
1263
1821
  * Whether the map can be rotated with a two-finger rotation gesture
1264
1822
  *
1265
1823
  * @type {Boolean}
1824
+ *
1825
+ * @since leaflet-rotate (v0.1.0)
1266
1826
  */
1267
1827
  touchRotate: false
1268
1828
  });
1269
1829
  L.Map.TouchRotate = L.Handler.extend({
1270
- addHooks: function() {
1830
+ /**
1831
+ * Enable rotation handling in the map's shared touch gesture handler.
1832
+ *
1833
+ * @since leaflet-rotate (v0.1.0)
1834
+ */
1835
+ addHooks() {
1271
1836
  this._map.touchGestures.enable();
1272
1837
  this._map.touchGestures.rotate = true;
1273
1838
  },
1274
- removeHooks: function() {
1839
+ /**
1840
+ * Disable rotation handling without affecting pinch zoom.
1841
+ *
1842
+ * @since leaflet-rotate (v0.1.0)
1843
+ */
1844
+ removeHooks() {
1275
1845
  this._map.touchGestures.rotate = false;
1276
1846
  }
1277
1847
  });
@@ -1280,33 +1850,170 @@ var leafletRotate = (() => {
1280
1850
  // src/map/handler/ShiftKeyRotate.js
1281
1851
  L.Map.mergeOptions({
1282
1852
  /**
1283
- * Whether the map can be rotated with shift + wheel scroll
1853
+ * Whether Shift+wheel changes the map bearing.
1854
+ *
1855
+ * @type {Boolean}
1856
+ *
1857
+ * @since leaflet-rotate (v0.1.0)
1858
+ */
1859
+ shiftKeyRotate: true,
1860
+ /**
1861
+ * Whether Ctrl+drag changes bearing and pitch.
1862
+ *
1284
1863
  * @type {Boolean}
1864
+ *
1865
+ * @since leaflet-rotate (0.5.0)
1285
1866
  */
1286
- shiftKeyRotate: true
1867
+ pitchDrag: true
1287
1868
  });
1288
1869
  L.Map.ShiftKeyRotate = L.Handler.extend({
1289
- addHooks: function() {
1290
- L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
1291
- this._map.shiftKeyRotate.rotate = true;
1870
+ /**
1871
+ * @param {L.Map} map Map controlled by this handler.
1872
+ *
1873
+ * @since leaflet-rotate (v0.1.0)
1874
+ */
1875
+ initialize(map) {
1876
+ this._map = map;
1877
+ this._onMouseDownBound = this._onMouseDown.bind(this);
1878
+ this._onMouseMoveBound = this._onMouseMove.bind(this);
1879
+ this._onMouseUpBound = this._onMouseUp.bind(this);
1292
1880
  },
1293
- removeHooks: function() {
1881
+ /**
1882
+ * Attach enabled wheel and pointer/mouse gesture listeners.
1883
+ *
1884
+ * @since leaflet-rotate (v0.1.0)
1885
+ */
1886
+ addHooks() {
1887
+ if (this._map.options.shiftKeyRotate) {
1888
+ L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
1889
+ }
1890
+ if (this._map.options.pitchDrag) {
1891
+ this.addPitchHooks();
1892
+ }
1893
+ },
1894
+ /**
1895
+ * Detach gesture listeners and cancel any active drag.
1896
+ *
1897
+ * @since leaflet-rotate (v0.1.0)
1898
+ */
1899
+ removeHooks() {
1294
1900
  L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this);
1295
- this._map.shiftKeyRotate.rotate = false;
1901
+ this.removePitchHooks();
1902
+ },
1903
+ /**
1904
+ * Listen for the start of Ctrl+drag pitch gestures.
1905
+ *
1906
+ * @since leaflet-rotate (0.5.0)
1907
+ */
1908
+ addPitchHooks() {
1909
+ if (this._pitchHooksAdded) {
1910
+ return;
1911
+ }
1912
+ this._pitchEvents = L.version.startsWith("2.") ? ["pointerdown", "pointermove", "pointerup pointercancel"] : ["mousedown", "mousemove", "mouseup"];
1913
+ this._map._container.addEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
1914
+ this._pitchHooksAdded = true;
1915
+ },
1916
+ /**
1917
+ * Stop listening for pitch gestures and remove active document listeners.
1918
+ *
1919
+ * @since leaflet-rotate (0.5.0)
1920
+ */
1921
+ removePitchHooks() {
1922
+ if (!this._pitchHooksAdded) {
1923
+ return;
1924
+ }
1925
+ this._map._container.removeEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
1926
+ this._onMouseUp();
1927
+ delete this._pitchEvents;
1928
+ this._pitchHooksAdded = false;
1296
1929
  },
1297
- _handleShiftScroll: function(e) {
1930
+ /**
1931
+ * Rotate the map in fixed increments while Shift is held during wheel input.
1932
+ *
1933
+ * @param {WheelEvent} e Wheel event from the map container.
1934
+ *
1935
+ * @since leaflet-rotate (v0.1.0)
1936
+ */
1937
+ _handleShiftScroll(e) {
1298
1938
  if (e.shiftKey) {
1299
1939
  e.preventDefault();
1300
1940
  this._map.scrollWheelZoom.disable();
1301
- this._map.setBearing(this._map._bearing * L.DomUtil.RAD_TO_DEG + Math.sign(e.deltaY) * 5);
1941
+ this._map.setBearing(this._map.getBearing() + Math.sign(e.deltaY) * 5);
1302
1942
  } else {
1303
1943
  this._map.scrollWheelZoom.enable();
1304
1944
  }
1945
+ },
1946
+ /**
1947
+ * Start Ctrl+drag and capture subsequent movement outside the map.
1948
+ *
1949
+ * @param {MouseEvent|PointerEvent} e Gesture start event.
1950
+ *
1951
+ * @since leaflet-rotate (0.5.0)
1952
+ */
1953
+ _onMouseDown(e) {
1954
+ if (!this._map._rotate || !this._map.options.pitchDrag || !e.ctrlKey || e.button !== 0) {
1955
+ return;
1956
+ }
1957
+ const scrollWheelZoom = this._map.scrollWheelZoom;
1958
+ if (scrollWheelZoom && scrollWheelZoom._timer) {
1959
+ clearTimeout(scrollWheelZoom._timer);
1960
+ scrollWheelZoom._timer = null;
1961
+ scrollWheelZoom._delta = 0;
1962
+ scrollWheelZoom._startTime = null;
1963
+ }
1964
+ e.preventDefault();
1965
+ e.stopPropagation();
1966
+ e.stopImmediatePropagation();
1967
+ this._lastX = e.clientX;
1968
+ this._lastY = e.clientY;
1969
+ document.addEventListener(this._pitchEvents[1], this._onMouseMoveBound);
1970
+ for (const event of this._pitchEvents[2].split(" ")) {
1971
+ document.addEventListener(event, this._onMouseUpBound);
1972
+ }
1973
+ },
1974
+ /**
1975
+ * Apply pointer movement to pitch (vertical) and bearing (horizontal).
1976
+ *
1977
+ * @param {MouseEvent|PointerEvent} e Gesture movement event.
1978
+ *
1979
+ * @since leaflet-rotate (0.5.0)
1980
+ */
1981
+ _onMouseMove(e) {
1982
+ this._map._setPitchAndBearing(
1983
+ this._map.getPitch() + (this._lastY - e.clientY) * 0.5,
1984
+ this._map.getBearing() + (e.clientX - this._lastX) * 0.8
1985
+ );
1986
+ this._lastX = e.clientX;
1987
+ this._lastY = e.clientY;
1988
+ e.preventDefault();
1989
+ },
1990
+ /**
1991
+ * End Ctrl+drag and clear document-level listeners.
1992
+ *
1993
+ * @since leaflet-rotate (0.5.0)
1994
+ */
1995
+ _onMouseUp() {
1996
+ if (!this._pitchEvents) {
1997
+ return;
1998
+ }
1999
+ document.removeEventListener(this._pitchEvents[1], this._onMouseMoveBound);
2000
+ for (const event of this._pitchEvents[2].split(" ")) {
2001
+ document.removeEventListener(event, this._onMouseUpBound);
2002
+ }
2003
+ delete this._lastX;
2004
+ delete this._lastY;
1305
2005
  }
1306
2006
  });
1307
- L.Map.addInitHook("addHandler", "shiftKeyRotate", L.Map.ShiftKeyRotate);
1308
2007
  L.Map.addInitHook(function() {
1309
- if (this.scrollWheelZoom.enabled() && this.shiftKeyRotate.enabled()) {
2008
+ if (this.options.shiftKeyRotate) {
2009
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
2010
+ this.shiftKeyRotate.enable();
2011
+ } else if (this.options.pitchDrag) {
2012
+ this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
2013
+ this.shiftKeyRotate.addPitchHooks();
2014
+ this.on("unload", this.shiftKeyRotate.removePitchHooks, this.shiftKeyRotate);
2015
+ }
2016
+ if (this.scrollWheelZoom.enabled() && this.options.shiftKeyRotate) {
1310
2017
  this.scrollWheelZoom.disable();
1311
2018
  this.scrollWheelZoom.enable();
1312
2019
  }
@@ -1322,6 +2029,8 @@ var leafletRotate = (() => {
1322
2029
  * for touch-capable web browsers.
1323
2030
  *
1324
2031
  * @type {(Boolean|String)}
2032
+ *
2033
+ * @since leaflet-rotate (v0.1.0)
1325
2034
  */
1326
2035
  touchZoom: L.Browser.touch,
1327
2036
  /**
@@ -1332,17 +2041,29 @@ var leafletRotate = (() => {
1332
2041
  * when pinch-zooming.
1333
2042
  *
1334
2043
  * @type {Boolean}
2044
+ *
2045
+ * @since leaflet-rotate (v0.1.0)
1335
2046
  */
1336
2047
  bounceAtZoomLimits: false
1337
2048
  });
1338
2049
  L.Map.TouchZoom = L.Handler.extend({
1339
- addHooks: function() {
1340
- L.DomUtil.addClass(this._map._container, "leaflet-touch-zoom");
2050
+ /**
2051
+ * Enable pinch zoom handling and mark the shared touch handler as active.
2052
+ *
2053
+ * @since leaflet-rotate (v0.1.0)
2054
+ */
2055
+ addHooks() {
2056
+ this._map._container.classList.add("leaflet-touch-zoom");
1341
2057
  this._map.touchGestures.enable();
1342
2058
  this._map.touchGestures.zoom = true;
1343
2059
  },
1344
- removeHooks: function() {
1345
- L.DomUtil.removeClass(this._map._container, "leaflet-touch-zoom");
2060
+ /**
2061
+ * Disable pinch zoom handling while preserving touch rotation state.
2062
+ *
2063
+ * @since leaflet-rotate (v0.1.0)
2064
+ */
2065
+ removeHooks() {
2066
+ this._map._container.classList.remove("leaflet-touch-zoom");
1346
2067
  this._map.touchGestures.zoom = false;
1347
2068
  }
1348
2069
  });
@@ -1354,9 +2075,18 @@ var leafletRotate = (() => {
1354
2075
  position: "topleft",
1355
2076
  closeOnZeroBearing: true
1356
2077
  },
1357
- onAdd: function(map) {
1358
- var container = this._container = L.DomUtil.create("div", "leaflet-control-rotate leaflet-bar");
1359
- var arrow = this._arrow = L.DomUtil.create("span", "leaflet-control-rotate-arrow");
2078
+ /**
2079
+ * Create the control element and subscribe to map rotation.
2080
+ *
2081
+ * @param {L.Map} map
2082
+ *
2083
+ * @returns {HTMLElement} Control container.
2084
+ *
2085
+ * @since leaflet-rotate (v0.1.0)
2086
+ */
2087
+ onAdd(map) {
2088
+ const container = this._container = L.DomUtil.create("div", "leaflet-control-rotate leaflet-bar");
2089
+ const arrow = this._arrow = L.DomUtil.create("span", "leaflet-control-rotate-arrow");
1360
2090
  arrow.style.backgroundImage = `url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")`;
1361
2091
  arrow.style.cursor = "grab";
1362
2092
  arrow.style.display = "block";
@@ -1364,14 +2094,11 @@ var leafletRotate = (() => {
1364
2094
  arrow.style.height = "100%";
1365
2095
  arrow.style.backgroundRepeat = "no-repeat";
1366
2096
  arrow.style.backgroundPosition = "50%";
1367
- var link = this._link = L.DomUtil.create("a", "leaflet-control-rotate-toggle", container);
2097
+ const link = this._link = L.DomUtil.create("a", "leaflet-control-rotate-toggle", container);
1368
2098
  link.appendChild(arrow);
1369
2099
  link.href = "#";
1370
2100
  link.title = "Rotate map";
1371
2101
  L.DomEvent.on(link, "dblclick", L.DomEvent.stopPropagation).on(link, "mousedown", this._handleMouseDown, this).on(link, "click", L.DomEvent.stop).on(link, "click", this._cycleState, this).on(link, "click", this._refocusOnMap, this);
1372
- if (!L.Browser.any3d) {
1373
- L.DomUtil.addClass(link, "leaflet-disabled");
1374
- }
1375
2102
  this._restyle();
1376
2103
  map.on("rotate", this._restyle, this);
1377
2104
  this._follow = false;
@@ -1381,33 +2108,69 @@ var leafletRotate = (() => {
1381
2108
  }
1382
2109
  return container;
1383
2110
  },
1384
- onRemove: function(map) {
2111
+ /**
2112
+ * Remove the map event listener when the control is detached.
2113
+ *
2114
+ * @param {L.Map} map
2115
+ *
2116
+ * @since leaflet-rotate (v0.1.0)
2117
+ */
2118
+ onRemove(map) {
1385
2119
  map.off("rotate", this._restyle, this);
1386
2120
  },
1387
- _handleMouseDown: function(e) {
2121
+ /**
2122
+ * Begin dragging the control's bearing indicator.
2123
+ *
2124
+ * @param {MouseEvent} e Mouse-down event.
2125
+ *
2126
+ * @since leaflet-rotate (v0.1.0)
2127
+ */
2128
+ _handleMouseDown(e) {
1388
2129
  L.DomEvent.stop(e);
1389
2130
  this.dragging = true;
1390
2131
  this.dragstartX = e.pageX;
1391
2132
  this.dragstartY = e.pageY;
1392
2133
  L.DomEvent.on(document, "mousemove", this._handleMouseDrag, this).on(document, "mouseup", this._handleMouseUp, this);
1393
2134
  },
1394
- _handleMouseUp: function(e) {
2135
+ /**
2136
+ * End bearing-indicator dragging and remove document listeners.
2137
+ *
2138
+ * @param {MouseEvent} e Mouse-up event.
2139
+ *
2140
+ * @since leaflet-rotate (v0.1.0)
2141
+ */
2142
+ _handleMouseUp(e) {
1395
2143
  L.DomEvent.stop(e);
1396
2144
  this.dragging = false;
1397
2145
  L.DomEvent.off(document, "mousemove", this._handleMouseDrag, this).off(document, "mouseup", this._handleMouseUp, this);
1398
2146
  },
1399
- _handleMouseDrag: function(e) {
2147
+ /**
2148
+ * Set bearing from the horizontal drag distance.
2149
+ *
2150
+ * @param {MouseEvent} e Mouse-move event.
2151
+ *
2152
+ * @since leaflet-rotate (v0.1.0)
2153
+ */
2154
+ _handleMouseDrag(e) {
1400
2155
  if (!this.dragging) {
1401
2156
  return;
1402
2157
  }
1403
- var deltaX = e.clientX - this.dragstartX;
2158
+ const deltaX = e.clientX - this.dragstartX;
1404
2159
  this._map.setBearing(deltaX);
1405
2160
  },
1406
- _cycleState: function(ev) {
2161
+ /**
2162
+ * Cycle through touch, compass, and locked rotation modes.
2163
+ * Compass mode may require permission from the browser.
2164
+ *
2165
+ * @param {Event} ev Control click event.
2166
+ *
2167
+ * @since leaflet-rotate (v0.1.0)
2168
+ */
2169
+ _cycleState(ev) {
1407
2170
  if (!this._map) {
1408
2171
  return;
1409
2172
  }
1410
- var map = this._map;
2173
+ const map = this._map;
1411
2174
  if (!map.touchRotate.enabled() && !map.compassBearing.enabled()) {
1412
2175
  map.touchRotate.enable();
1413
2176
  } else if (!map.compassBearing.enabled()) {
@@ -1422,12 +2185,19 @@ var leafletRotate = (() => {
1422
2185
  }
1423
2186
  this._restyle();
1424
2187
  },
1425
- _restyle: function() {
2188
+ /**
2189
+ * Update the control icon and visibility to match the current mode.
2190
+ *
2191
+ * @listens L.Map#rotate
2192
+ *
2193
+ * @since leaflet-rotate (v0.1.0)
2194
+ */
2195
+ _restyle() {
1426
2196
  if (!this._map.options.rotate) {
1427
- L.DomUtil.addClass(this._link, "leaflet-disabled");
2197
+ this._link.classList.add("leaflet-disabled");
1428
2198
  } else {
1429
- var map = this._map;
1430
- var bearing = map.getBearing();
2199
+ const map = this._map;
2200
+ const bearing = map.getBearing();
1431
2201
  this._arrow.style.transform = "rotate(" + bearing + "deg)";
1432
2202
  if (bearing && this.options.closeOnZeroBearing) {
1433
2203
  this._container.style.display = "block";
@@ -1445,16 +2215,24 @@ var leafletRotate = (() => {
1445
2215
  }
1446
2216
  }
1447
2217
  });
1448
- L.control.rotate = function(options) {
1449
- return new L.Control.Rotate(options);
1450
- };
2218
+ if (L.control) {
2219
+ L.control.rotate = (options) => new L.Control.Rotate(options);
2220
+ }
1451
2221
  L.Map.mergeOptions({
2222
+ /**
2223
+ * Add the rotation control automatically. Set to `false` to disable,
2224
+ * or pass an options object to configure the control.
2225
+ *
2226
+ * @type {boolean|Object}
2227
+ *
2228
+ * @since leaflet-rotate (v0.1.0)
2229
+ */
1452
2230
  rotateControl: true
1453
2231
  });
1454
2232
  L.Map.addInitHook(function() {
1455
2233
  if (this.options.rotateControl) {
1456
- var options = typeof this.options.rotateControl === "object" ? this.options.rotateControl : {};
1457
- this.rotateControl = L.control.rotate(options);
2234
+ const options = typeof this.options.rotateControl === "object" ? this.options.rotateControl : {};
2235
+ this.rotateControl = new L.Control.Rotate(options);
1458
2236
  this.addControl(this.rotateControl);
1459
2237
  }
1460
2238
  });