leaflet-rotate 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,11 +30,15 @@ var leafletRotate = (() => {
30
30
  * Resets the 3D CSS transform of `el` so it is translated by `offset` pixels,
31
31
  * optionally scaled by `scale`, and rotated around `pivot` when `bearing` is set.
32
32
  *
33
- * @param {HTMLElement} el
34
- * @param {L.Point} offset
35
- * @param {Number} scale
36
- * @param {Number} bearing
37
- * @param {L.Point} pivot
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)
38
42
  */
39
43
  setTransform(el, offset, scale, bearing, pivot) {
40
44
  let pos = offset || new L.Point(0, 0);
@@ -48,11 +52,15 @@ var leafletRotate = (() => {
48
52
  * Sets the position of `el` to `point`, using CSS transforms or top/left positioning
49
53
  * depending on browser support, and optionally rotating it around `pivot`.
50
54
  *
51
- * @param {HTMLElement} el
52
- * @param {L.Point} point
53
- * @param {Number} bearing
54
- * @param {L.Point} pivot
55
- * @param {Number} scale
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}
62
+ *
63
+ * @since leaflet-rotate (v0.1.0)
56
64
  */
57
65
  setPosition(el, point, bearing, pivot, scale) {
58
66
  if (!bearing) {
@@ -64,8 +72,11 @@ var leafletRotate = (() => {
64
72
  /**
65
73
  * Returns the position assigned to `el`.
66
74
  *
67
- * @param {HTMLElement} el
68
- * @returns {L.Point}
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)
69
80
  */
70
81
  getPosition(el) {
71
82
  return el._leaflet_pos || domUtilProto.getPosition(el);
@@ -88,7 +99,10 @@ var leafletRotate = (() => {
88
99
  * ⎡ x' ⎤ = ⎡ cos θ -sin θ ⎤ ⎡ x ⎤
89
100
  * ⎣ y' ⎦ ⎣ sin θ cos θ ⎦ ⎣ y ⎦
90
101
  *
91
- * @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)
92
106
  */
93
107
  rotate(theta) {
94
108
  return this.rotateFrom(theta, new L.Point(0, 0));
@@ -102,10 +116,12 @@ var leafletRotate = (() => {
102
116
  *
103
117
  * same as `this.subtract(pivot).rotate(theta).add(pivot)`
104
118
  *
105
- * @param {Number} theta
106
- * @param {L.Point} pivot
119
+ * @param {number} theta Rotation angle in radians, counterclockwise.
120
+ * @param {L.Point} pivot Point to rotate around.
107
121
  *
108
122
  * @returns {L.Point}
123
+ *
124
+ * @since leaflet-rotate (v0.1.0)
109
125
  */
110
126
  rotateFrom(theta, pivot) {
111
127
  if (!theta) {
@@ -132,17 +148,41 @@ var leafletRotate = (() => {
132
148
  var divOverlayProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.DivOverlay.prototype));
133
149
  L.DivOverlay.include({
134
150
  /**
135
- * Update L.Popup and L.Tooltip anchor positions after
136
- * the map is moved by calling `map.setBearing(theta)`
151
+ * Extend overlay event subscriptions to refresh position after map rotation and pitch.
137
152
  *
138
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)
139
159
  */
140
160
  getEvents() {
141
- return Object.assign(divOverlayProto.getEvents.apply(this, arguments), { rotate: this._updatePosition });
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)
174
+ */
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
+ }
142
181
  },
143
182
  /**
144
- * 0. update element anchor point (divOverlayProto v1.9.3)
145
- * 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)
146
186
  */
147
187
  _updatePosition() {
148
188
  if (!this._map) {
@@ -170,8 +210,11 @@ var leafletRotate = (() => {
170
210
  var popupProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Popup.prototype));
171
211
  L.Popup.include({
172
212
  /**
173
- * 0. update element anchor point (popupProto v1.9.3)
174
- * 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)
175
218
  */
176
219
  _animateZoom(e) {
177
220
  popupProto._animateZoom.apply(this, arguments);
@@ -182,9 +225,13 @@ var leafletRotate = (() => {
182
225
  }
183
226
  },
184
227
  /**
185
- * Fix for L.popup({ keepInView = true })
228
+ * Pan the map when a popup with auto-pan enabled would extend beyond the viewport.
186
229
  *
187
230
  * @see https://github.com/fnicollet/Leaflet/pull/21
231
+ *
232
+ * @fires L.Map#autopanstart
233
+ *
234
+ * @since leaflet-rotate (v0.1.0)
188
235
  */
189
236
  _adjustPan() {
190
237
  if (!this.options.autoPan || this._map._panAnim && this._map._panAnim._inProgress) {
@@ -222,6 +269,13 @@ var leafletRotate = (() => {
222
269
  // src/layer/Tooltip.js
223
270
  var tooltipProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Tooltip.prototype));
224
271
  L.Tooltip.include({
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
+ */
225
279
  _animateZoom(e) {
226
280
  if (!this._map._rotate) {
227
281
  return tooltipProto._animateZoom.apply(this, arguments);
@@ -230,6 +284,11 @@ var leafletRotate = (() => {
230
284
  pos = this._map.rotatedPointToMapPanePoint(pos);
231
285
  this._setPosition(pos);
232
286
  },
287
+ /**
288
+ * Recompute tooltip screen position after map movement.
289
+ *
290
+ * @since leaflet-rotate (v0.1.0)
291
+ */
233
292
  _updatePosition() {
234
293
  if (!this._map._rotate) {
235
294
  return tooltipProto._updatePosition.apply(this, arguments);
@@ -243,6 +302,14 @@ var leafletRotate = (() => {
243
302
  // src/layer/marker/Icon.js
244
303
  var iconProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Icon.prototype));
245
304
  L.Icon.include({
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
+ */
246
313
  _setIconStyles(img, name) {
247
314
  const options = this.options;
248
315
  let sizeOption = options[name + "Size"];
@@ -267,27 +334,38 @@ var leafletRotate = (() => {
267
334
  var markerProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Marker.prototype));
268
335
  L.Marker.mergeOptions({
269
336
  /**
270
- * Rotation of this marker in rad
337
+ * Rotation of the marker icon in radians.
271
338
  *
272
339
  * @type {Number}
340
+ *
341
+ * @since leaflet-rotate (v0.1.0)
273
342
  */
274
343
  rotation: 0,
275
344
  /**
276
- * 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.
277
349
  *
278
350
  * @type {Boolean}
351
+ *
352
+ * @since leaflet-rotate (v0.1.0)
279
353
  */
280
354
  rotateWithView: false,
281
355
  /**
282
- * Flip a rotated marker when needed to keep it upright
356
+ * Flip the icon when needed to keep it upright while map-aligned.
283
357
  *
284
358
  * @type {Boolean}
359
+ *
360
+ * @since leaflet-rotate (v0.1.0)
285
361
  */
286
362
  keepUpright: false,
287
363
  /**
288
- * Scale of the marker icon
364
+ * Optional scale of the marker icon and shadow.
289
365
  *
290
366
  * @type {Number}
367
+ *
368
+ * @since leaflet-rotate (v0.1.0)
291
369
  */
292
370
  scale: void 0
293
371
  });
@@ -299,6 +377,13 @@ var leafletRotate = (() => {
299
377
  // }
300
378
  // this._draggable.updateMapBearing(this._marker._map._bearing);
301
379
  // },
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
+ */
302
387
  _onDrag(e) {
303
388
  const marker = this._marker, rotated_marker = marker.options.rotation || marker.options.rotateWithView, shadow = marker._shadow;
304
389
  let iconPos = L.DomUtil.getPosition(marker._icon);
@@ -316,6 +401,13 @@ var leafletRotate = (() => {
316
401
  else marker.fire("move", e);
317
402
  marker.fire("drag", e);
318
403
  },
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
+ */
319
411
  _onDragEnd(e) {
320
412
  if (this._marker._map._rotate) {
321
413
  this._marker.update();
@@ -325,14 +417,42 @@ var leafletRotate = (() => {
325
417
  };
326
418
  L.Marker.include({
327
419
  /**
328
- * Update L.Marker anchor position after the map
329
- * is moved by calling `map.setBearing(theta)`
420
+ * Extend marker event subscriptions to reposition its icon after bearing or pitch changes.
330
421
  *
331
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)
332
428
  */
333
429
  getEvents() {
334
- return Object.assign(markerProto.getEvents.apply(this, arguments), { rotate: this.update });
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;
335
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
+ }
450
+ },
451
+ /**
452
+ * Install rotation-aware drag callbacks when marker dragging is enabled.
453
+ *
454
+ * @since leaflet-rotate (v0.1.0)
455
+ */
336
456
  _initInteraction() {
337
457
  const ret = markerProto._initInteraction.apply(this, arguments);
338
458
  if (this.dragging && this.dragging.enabled() && this._map && this._map._rotate) {
@@ -347,6 +467,13 @@ var leafletRotate = (() => {
347
467
  }
348
468
  return ret;
349
469
  },
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
+ */
350
477
  _setPos(pos) {
351
478
  if (this._map._rotate) {
352
479
  pos = this._map.rotatedPointToMapPanePoint(pos);
@@ -377,6 +504,15 @@ var leafletRotate = (() => {
377
504
  // }
378
505
  // this._icon.style.zIndex = Math.round(this._zIndex + offset);
379
506
  // },
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
+ */
380
516
  setRotation(rotation) {
381
517
  this.options.rotation = rotation;
382
518
  this.update();
@@ -387,10 +523,13 @@ var leafletRotate = (() => {
387
523
  var gridLayerProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.GridLayer.prototype));
388
524
  L.GridLayer.include({
389
525
  /**
390
- * Redraw L.TileLayer bounds after the map is
391
- * moved by just calling `map.setBearing(theta)`
526
+ * Add a throttled tile refresh after map bearing changes.
392
527
  *
393
528
  * @listens L.Map~rotate
529
+ *
530
+ * @returns {Object} Leaflet event map for the grid layer.
531
+ *
532
+ * @since leaflet-rotate (v0.1.0)
394
533
  */
395
534
  getEvents() {
396
535
  const events = gridLayerProto.getEvents.apply(this, arguments);
@@ -402,6 +541,15 @@ var leafletRotate = (() => {
402
541
  }
403
542
  return events;
404
543
  },
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
+ */
405
553
  _getTiledPixelBounds(center) {
406
554
  if (!this._map._rotate) {
407
555
  return gridLayerProto._getTiledPixelBounds.apply(this, arguments);
@@ -414,21 +562,45 @@ var leafletRotate = (() => {
414
562
  var rendererProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Renderer.prototype));
415
563
  var rendererBase = Object.getPrototypeOf(L.Renderer.prototype);
416
564
  var rendererGetEvents = rendererProto.getEvents || rendererBase.getEvents;
565
+ var rendererOnMoveEnd = rendererProto._onMoveEnd || rendererBase._onMoveEnd;
417
566
  L.Renderer.include({
418
567
  /**
419
- * Redraw L.Canvas and L.SVG renderer bounds after the
420
- * map is moved by just calling `map.setBearing(theta)`
568
+ * Subscribe the renderer to map rotation so its visible bounds are recalculated.
421
569
  *
422
570
  * @listens L.Map~rotate
571
+ *
572
+ * @returns {Object} Leaflet event map for the renderer.
573
+ *
574
+ * @since leaflet-rotate (v0.1.0)
423
575
  */
424
576
  getEvents() {
425
577
  const events = rendererGetEvents.apply(this, arguments);
426
- return L.version.startsWith("2.") ? events : Object.assign(events, { rotate: this._update });
578
+ return Object.assign(events, { rotate: this._onRotate });
427
579
  },
428
580
  /**
429
- * Fix for `map.flyTo()` when `false === map.options.zoomAnimation`
581
+ * Reproject vector paths and refresh renderer bounds after bearing changes.
582
+ *
583
+ * @since leaflet-rotate (v0.1.0)
584
+ */
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
+ }
595
+ },
596
+ /**
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.
430
600
  *
431
601
  * @see https://github.com/Leaflet/Leaflet/pull/8794
602
+ *
603
+ * @since leaflet-rotate (v0.2.0)
432
604
  */
433
605
  onAdd() {
434
606
  rendererProto.onAdd.apply(this, arguments);
@@ -437,6 +609,8 @@ var leafletRotate = (() => {
437
609
  }
438
610
  },
439
611
  /**
612
+ * Update the renderer transform during zoom animation, accounting for rotated coordinates.
613
+ *
440
614
  * @FIXME layer drifts on `map.setZoom()` (eg. zoom during animation)
441
615
  *
442
616
  * the main cause seems to be related to `this._updateTransform(path._center, path._zoom))`
@@ -451,9 +625,14 @@ var leafletRotate = (() => {
451
625
  * @see https://github.com/Leaflet/Leaflet/pull/8103
452
626
  * @see https://github.com/Leaflet/Leaflet/issues/7466
453
627
  *
454
- * @TODO rechek this changes from leaflet@v1.9.3
628
+ * @TODO recheck these changes against leaflet@v1.9.3
455
629
  *
456
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)
457
636
  */
458
637
  _updateTransform(center, zoom) {
459
638
  if (L.version.startsWith("2.")) {
@@ -473,9 +652,16 @@ var leafletRotate = (() => {
473
652
  const scale = this._map.getZoomScale(zoom, this._zoom), offset = this._map._latLngToNewLayerPoint(this._topLeft, zoom, center);
474
653
  L.DomUtil.setTransform(this._container, offset, scale);
475
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
+ */
476
662
  _onMoveEnd(ev) {
477
663
  if (!L.version.startsWith("2.") || !this._map._rotate) {
478
- return rendererBase._onMoveEnd.apply(this, arguments);
664
+ return rendererOnMoveEnd.apply(this, arguments);
479
665
  }
480
666
  this._bounds = this._map._getPaddedPixelBounds(this.options.padding);
481
667
  this._topLeft = this._map.layerPointToLatLng(this._bounds.min);
@@ -486,6 +672,11 @@ var leafletRotate = (() => {
486
672
  this._resizeContainer();
487
673
  this._onSettled(ev);
488
674
  },
675
+ /**
676
+ * Resize the renderer container to cover the pitch-aware map bounds.
677
+ *
678
+ * @since leaflet-rotate (v0.4.0)
679
+ */
489
680
  _resizeContainer() {
490
681
  if (!L.version.startsWith("2.") || !this._map._rotate || !this._bounds) {
491
682
  return rendererBase._resizeContainer.apply(this, arguments);
@@ -530,6 +721,11 @@ var leafletRotate = (() => {
530
721
  // this._layers[id]._update();
531
722
  // }
532
723
  // },
724
+ /**
725
+ * Recalculate renderer bounds and cached view state.
726
+ *
727
+ * @since leaflet-rotate (v0.1.0)
728
+ */
533
729
  _update() {
534
730
  if (!this._map._rotate) {
535
731
  return rendererProto._update.apply(this, arguments);
@@ -543,8 +739,35 @@ var leafletRotate = (() => {
543
739
 
544
740
  // src/map/Map.js
545
741
  var mapProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Map.prototype));
546
- L.Map.mergeOptions({ rotate: false, bearing: 0 });
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
+ });
547
762
  L.Map.include({
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
+ */
548
771
  _animateZoomNoDelay(center, zoom) {
549
772
  if (!this._mapPane) {
550
773
  return;
@@ -556,17 +779,24 @@ var leafletRotate = (() => {
556
779
  this._onZoomTransitionEnd();
557
780
  },
558
781
  /**
559
- * @param {(HTMLElement|String)} id html selector
560
- * @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)
561
788
  */
562
789
  initialize(id, options) {
563
790
  if (options.rotate) {
564
791
  this._rotate = true;
565
792
  this._bearing = 0;
793
+ this._pitch = 0;
566
794
  }
567
795
  mapProto.initialize.apply(this, arguments);
568
796
  if (this.options.rotate) {
797
+ this.on("resize", this._updatePitchTransform, this);
569
798
  this.setBearing(this.options.bearing);
799
+ this.setPitch(this.options.pitch);
570
800
  }
571
801
  },
572
802
  /**
@@ -574,26 +804,41 @@ var leafletRotate = (() => {
574
804
  * returns the corresponding pixel coordinate relative to
575
805
  * the [origin pixel](#map-getpixelorigin).
576
806
  *
577
- * @param {L.Point} point pixel screen coordinates
578
- * @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)
579
812
  */
580
813
  containerPointToLayerPoint(point) {
581
814
  if (!this._rotate) {
582
815
  return mapProto.containerPointToLayerPoint.apply(this, arguments);
583
816
  }
817
+ if (this._pitch) {
818
+ return this._transformPitchPoint(
819
+ toPoint(point).subtract(this._getMapPanePos()),
820
+ this._pitchInverseTransform
821
+ );
822
+ }
584
823
  return toPoint(point).subtract(this._getMapPanePos()).rotateFrom(-this._bearing, this._getRotatePanePos()).subtract(this._getRotatePanePos());
585
824
  },
586
825
  /**
587
826
  * Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
588
827
  * returns the corresponding pixel coordinate relative to the map container.
589
828
  *
590
- * @param {L.Point} point pixel screen coordinates
591
- * @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)
592
834
  */
593
835
  layerPointToContainerPoint(point) {
594
836
  if (!this._rotate) {
595
837
  return mapProto.layerPointToContainerPoint.apply(this, arguments);
596
838
  }
839
+ if (this._pitch) {
840
+ return this._transformPitchPoint(point, this._pitchTransform).add(this._getMapPanePos());
841
+ }
597
842
  return toPoint(point).add(this._getRotatePanePos()).rotateFrom(this._bearing, this._getRotatePanePos()).add(this._getMapPanePos());
598
843
  },
599
844
  /**
@@ -603,12 +848,16 @@ var leafletRotate = (() => {
603
848
  * (rotatePane) --> (mapPane)
604
849
  * (rotatePane) --> (norotatePane)
605
850
  *
606
- * @param {L.Point} point pixel screen coordinates
607
- * @returns {L.Point}
851
+ * @param {L.Point|number[]} point Point in the rotated pane.
852
+ *
853
+ * @returns {L.Point} Corresponding point in map-pane coordinates.
608
854
  *
609
- * @since leaflet-rotate (v0.1)
855
+ * @since leaflet-rotate (v0.1.0)
610
856
  */
611
857
  rotatedPointToMapPanePoint(point) {
858
+ if (this._pitch) {
859
+ return this._transformPitchPoint(point, this._pitchTransform);
860
+ }
612
861
  return toPoint(point).rotate(this._bearing)._add(this._getRotatePanePos());
613
862
  },
614
863
  /**
@@ -618,11 +867,16 @@ var leafletRotate = (() => {
618
867
  * (mapPane) --> (rotatePane)
619
868
  * (norotatePane) --> (rotatePane)
620
869
  *
621
- * @param {L.Point} point pixel screen coordinates
870
+ * @param {L.Point|number[]} point Point in map-pane coordinates.
622
871
  *
623
- * @since leaflet-rotate (v0.1)
872
+ * @returns {L.Point} Corresponding point in the rotated pane.
873
+ *
874
+ * @since leaflet-rotate (v0.1.0)
624
875
  */
625
876
  mapPanePointToRotatedPoint(point) {
877
+ if (this._pitch) {
878
+ return this._transformPitchPoint(point, this._pitchInverseTransform);
879
+ }
626
880
  return toPoint(point)._subtract(this._getRotatePanePos()).rotate(-this._bearing);
627
881
  },
628
882
  // latLngToLayerPoint(latlng) {
@@ -638,10 +892,11 @@ var leafletRotate = (() => {
638
892
  *
639
893
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/map/Map.js#L1114-L1135
640
894
  *
641
- * @param {L.LatLngBounds} bounds
642
- * @returns {L.Bounds}
895
+ * @param {L.LatLngBounds} bounds Geographic bounds to project.
643
896
  *
644
- * @since leaflet-rotate (v0.2)
897
+ * @returns {L.Bounds} Axis-aligned bounds in container-pixel coordinates.
898
+ *
899
+ * @since leaflet-rotate (v0.2.0)
645
900
  */
646
901
  mapBoundsToContainerBounds(bounds) {
647
902
  if (!this._rotate && mapProto.mapBoundsToContainerBounds) {
@@ -657,14 +912,13 @@ var leafletRotate = (() => {
657
912
  ]);
658
913
  },
659
914
  /**
660
- * Returns geographical bounds visible in the current map view
915
+ * Return the geographic bounds of the current viewport, accounting for rotation and pitch.
661
916
  *
662
- * @TODO find out if map bounds calculated by `L.Map::getBounds()`
663
- * function should match the `rotatePane` or `norotatePane` bounds
917
+ * The result is the smallest geographic bounds containing all four viewport corners.
664
918
  *
665
- * @see https://github.com/fnicollet/Leaflet/issues/7
919
+ * @returns {L.LatLngBounds} Geographic bounds visible in the map viewport.
666
920
  *
667
- * @returns {L.LatLngBounds}
921
+ * @since leaflet-rotate (v0.1.0)
668
922
  */
669
923
  getBounds() {
670
924
  if (!this._rotate) {
@@ -699,15 +953,19 @@ var leafletRotate = (() => {
699
953
  // return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
700
954
  // },
701
955
  /**
702
- * 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.
963
+ *
964
+ * @returns {void}
703
965
  *
704
- * @param {number} theta map degrees
705
- * @param {Object} [options] animation options
706
- * @param {boolean} [options.animate=false]
707
- * @param {number} [options.duration=0.25] animation duration in seconds
708
- * @param {number} [options.easeLinearity=0.5]
966
+ * @fires L.Map#rotate
709
967
  *
710
- * @since leaflet-rotate (v0.1)
968
+ * @since leaflet-rotate (v0.1.0)
711
969
  */
712
970
  setBearing(theta, options) {
713
971
  if (!this._rotate) {
@@ -746,20 +1004,145 @@ var leafletRotate = (() => {
746
1004
  }
747
1005
  this._setBearing(theta);
748
1006
  },
749
- _setBearing(theta) {
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
+ }
750
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);
751
- L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
752
1022
  this._pivot = center;
753
1023
  this._bearing = bearing;
754
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
+ }
755
1036
  this.fire("rotate");
756
1037
  },
757
1038
  /**
758
- * 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.
1132
+ *
1133
+ * @returns {number} Pitch angle in degrees.
1134
+ *
1135
+ * @since leaflet-rotate (0.5.0)
1136
+ */
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.
759
1142
  *
760
- * @returns {number} theta map degrees
1143
+ * @returns {number} Bearing in degrees, normalized to [0, 360).
761
1144
  *
762
- * @since leaflet-rotate (v0.1)
1145
+ * @since leaflet-rotate (v0.1.0)
763
1146
  */
764
1147
  getBearing() {
765
1148
  return this._bearing * (180 / Math.PI);
@@ -771,7 +1154,10 @@ var leafletRotate = (() => {
771
1154
  *
772
1155
  * @param {String} name leaflet pane
773
1156
  * @param {HTMLElement} [container] parent element
1157
+ *
774
1158
  * @returns {HTMLElement} pane container
1159
+ *
1160
+ * @since leaflet-rotate (v0.1.0)
775
1161
  */
776
1162
  // createPane(name, container) {
777
1163
  // if (!this._rotate || name == 'mapPane') {
@@ -801,6 +1187,12 @@ var leafletRotate = (() => {
801
1187
  * - tooltipPane [HTMLElement = 650] - Pane for tooltips.
802
1188
  * - popupPane [HTMLElement = 700] - Pane for popups.
803
1189
  */
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
+ */
804
1196
  _initPanes() {
805
1197
  const panes = this._panes = {};
806
1198
  this._paneRenderers = {};
@@ -839,13 +1231,15 @@ var leafletRotate = (() => {
839
1231
  *
840
1232
  * @see https://github.com/Raruto/leaflet-rotate/issues/18
841
1233
  *
842
- * @param {L.LatLng} latlng coordinates
843
- * @param {Object} [options={}] padding options
1234
+ * @param {L.LatLng} latlng Geographic point to keep visible.
1235
+ * @param {Object} [options={}] Padding options (`padding`, `paddingTopLeft`, `paddingBottomRight`).
844
1236
  *
845
1237
  * @returns {L.Map} current map instance
1238
+ *
1239
+ * @since leaflet-rotate (v0.1.4)
846
1240
  */
847
1241
  panInside(latlng, options) {
848
- 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) {
849
1243
  return mapProto.panInside.apply(this, arguments);
850
1244
  }
851
1245
  options = options || {};
@@ -871,9 +1265,12 @@ var leafletRotate = (() => {
871
1265
  *
872
1266
  * @see https://github.com/fnicollet/Leaflet/issues/7
873
1267
  *
874
- * @param {L.LatLngBounds} bounds coordinates
875
- * @param {Object} [options] pan options
876
- * @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)
877
1274
  */
878
1275
  // panInsideBounds(bounds, options) {
879
1276
  // this._enforcingBounds = true;
@@ -938,14 +1335,16 @@ var leafletRotate = (() => {
938
1335
  * instead returns the minimum zoom level on which the map view fits into
939
1336
  * the given bounds in its entirety.
940
1337
  *
941
- * @param {L.LatLngBounds} bounds
942
- * @param {Boolean} [inside=false]
943
- * @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.
944
1343
  *
945
- * @returns {Number} zoom level
1344
+ * @since leaflet-rotate (v0.1.0)
946
1345
  */
947
1346
  getBoundsZoom(bounds, inside, padding) {
948
- 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) {
949
1348
  return mapProto.getBoundsZoom.apply(this, arguments);
950
1349
  }
951
1350
  bounds = bounds instanceof L.LatLngBounds ? bounds : new L.LatLngBounds(bounds);
@@ -968,19 +1367,30 @@ var leafletRotate = (() => {
968
1367
  // return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
969
1368
  // },
970
1369
  /**
971
- * 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.
972
1371
  *
973
- * @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)
974
1377
  */
975
1378
  _getCenterOffset(latlng) {
976
1379
  let centerOffset = mapProto._getCenterOffset.apply(this, arguments);
977
- if (this._rotate) {
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) {
978
1384
  centerOffset = centerOffset.rotate(this._bearing);
979
1385
  }
980
1386
  return centerOffset;
981
1387
  },
982
1388
  /**
983
- * @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)
984
1394
  */
985
1395
  _getRotatePanePos() {
986
1396
  return this._rotatePanePos || new L.Point(0, 0);
@@ -989,6 +1399,16 @@ var leafletRotate = (() => {
989
1399
  // const topLeft = this._getNewPixelOrigin(center, zoom);
990
1400
  // return this.project(latlng, zoom)._subtract(topLeft);
991
1401
  //},
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
+ */
992
1412
  _getNewPixelOrigin(center, zoom) {
993
1413
  if (!this._rotate || L.version.startsWith("2.")) {
994
1414
  return mapProto._getNewPixelOrigin.apply(this, arguments);
@@ -997,7 +1417,14 @@ var leafletRotate = (() => {
997
1417
  return this.project(center, zoom).rotate(this._bearing)._subtract(viewHalf)._add(this._getMapPanePos())._add(this._getRotatePanePos()).rotate(-this._bearing)._round();
998
1418
  },
999
1419
  /**
1000
- * @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)
1001
1428
  *
1002
1429
  * @see src\layer\tile\GridLayer::_getTiledPixelBounds()
1003
1430
  */
@@ -1016,9 +1443,9 @@ var leafletRotate = (() => {
1016
1443
  return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
1017
1444
  },
1018
1445
  /**
1019
- * @since leaflet-rotate (v0.2)
1446
+ * @since leaflet-rotate (v0.2.0)
1020
1447
  *
1021
- * @return {L.Point} map pivot point (center)
1448
+ * @returns {L.Point} Map pivot point in pixel coordinates.
1022
1449
  */
1023
1450
  _getPixelCenter() {
1024
1451
  if (!this._rotate && mapProto._getPixelCenter) {
@@ -1027,9 +1454,13 @@ var leafletRotate = (() => {
1027
1454
  return this.getSize()._divideBy(2)._subtract(this._getMapPanePos());
1028
1455
  },
1029
1456
  /**
1030
- * @since leaflet-rotate (v0.2)
1031
- *
1032
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)
1033
1464
  */
1034
1465
  _getPaddedPixelBounds(padding) {
1035
1466
  if (!this._rotate && mapProto._getPaddedPixelBounds) {
@@ -1043,6 +1474,15 @@ var leafletRotate = (() => {
1043
1474
  this.containerPointToLayerPoint([padMax.x, padMax.y]).floor()
1044
1475
  ]);
1045
1476
  },
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
+ */
1046
1486
  _handleGeolocationResponse(pos) {
1047
1487
  if (!this._container._leaflet_id) {
1048
1488
  return;
@@ -1070,6 +1510,7 @@ var leafletRotate = (() => {
1070
1510
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/geo/LatLngBounds.js#L253-L264
1071
1511
  *
1072
1512
  * @param {L.Bounds} points
1513
+ *
1073
1514
  * @returns {L.Bounds}
1074
1515
  */
1075
1516
  // toCircumscribedBounds(points) {
@@ -1084,6 +1525,11 @@ var leafletRotate = (() => {
1084
1525
 
1085
1526
  // src/map/handler/CompassBearing.js
1086
1527
  L.Map.CompassBearing = L.Handler.extend({
1528
+ /**
1529
+ * @param {L.Map} map Map whose bearing follows device orientation.
1530
+ *
1531
+ * @since leaflet-rotate (v0.1.0)
1532
+ */
1087
1533
  initialize(map) {
1088
1534
  this._map = map;
1089
1535
  if ("ondeviceorientationabsolute" in window) {
@@ -1093,6 +1539,11 @@ var leafletRotate = (() => {
1093
1539
  }
1094
1540
  this._throttled = L.Util.throttle(this._onDeviceOrientation, 100, this);
1095
1541
  },
1542
+ /**
1543
+ * Subscribe to the supported device-orientation event, if available.
1544
+ *
1545
+ * @since leaflet-rotate (v0.1.0)
1546
+ */
1096
1547
  addHooks() {
1097
1548
  if (this._map._rotate && this.__deviceOrientationEvent) {
1098
1549
  L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this);
@@ -1100,12 +1551,19 @@ var leafletRotate = (() => {
1100
1551
  this.disable();
1101
1552
  }
1102
1553
  },
1554
+ /**
1555
+ * Unsubscribe from device orientation when the handler is disabled.
1556
+ *
1557
+ * @since leaflet-rotate (v0.1.0)
1558
+ */
1103
1559
  removeHooks() {
1104
1560
  if (this._map._rotate && this.__deviceOrientationEvent) {
1105
1561
  L.DomEvent.off(window, this.__deviceOrientationEvent, this._throttled, this);
1106
1562
  }
1107
1563
  },
1108
1564
  /**
1565
+ * Apply a device orientation reading to the map bearing.
1566
+ *
1109
1567
  * `DeviceOrientationEvent.absolute` - Indicates whether the device is providing absolute
1110
1568
  * orientation values (relatives to Magnetic North) or
1111
1569
  * using some arbitrary frame determined by the device.
@@ -1122,6 +1580,10 @@ var leafletRotate = (() => {
1122
1580
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/absolute
1123
1581
  * @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/alpha
1124
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)
1125
1587
  */
1126
1588
  _onDeviceOrientation(e) {
1127
1589
  let angle = e.webkitCompassHeading != null ? e.webkitCompassHeading : e.alpha;
@@ -1150,11 +1612,18 @@ var leafletRotate = (() => {
1150
1612
  *
1151
1613
  * @type {Boolean}
1152
1614
  *
1615
+ * @since leaflet-rotate (v0.1.0)
1616
+ *
1153
1617
  * @see https://developer.mozilla.org/docs/Web/API/MutationObserver
1154
1618
  */
1155
1619
  trackContainerMutation: false
1156
1620
  });
1157
1621
  L.Map.ContainerMutation = L.Handler.extend({
1622
+ /**
1623
+ * Start observing the map container's style attribute.
1624
+ *
1625
+ * @since leaflet-rotate (v0.1.0)
1626
+ */
1158
1627
  addHooks() {
1159
1628
  if (!this._observer) {
1160
1629
  this._observer = new MutationObserver(L.Util.bind(this._map.invalidateSize, this._map));
@@ -1167,6 +1636,11 @@ var leafletRotate = (() => {
1167
1636
  attributeFilter: ["style"]
1168
1637
  });
1169
1638
  },
1639
+ /**
1640
+ * Stop observing container mutations.
1641
+ *
1642
+ * @since leaflet-rotate (v0.1.0)
1643
+ */
1170
1644
  removeHooks() {
1171
1645
  this._observer.disconnect();
1172
1646
  }
@@ -1183,26 +1657,50 @@ var leafletRotate = (() => {
1183
1657
  }
1184
1658
  L.Map.mergeOptions({
1185
1659
  /**
1186
- * Set it to false if you don't want the map to
1187
- * zoom beyond min/max zoom and then bounce back
1188
- * when pinch-zooming.
1660
+ * Whether pinch zoom may temporarily exceed min/max zoom and bounce back.
1189
1661
  *
1190
1662
  * @type {Boolean}
1663
+ *
1664
+ * @since leaflet-rotate (v0.1.0)
1191
1665
  */
1192
1666
  bounceAtZoomLimits: true
1193
1667
  });
1194
1668
  L.Map.TouchGestures = L.Handler.extend({
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
+ */
1195
1676
  initialize(map) {
1196
1677
  this._map = map;
1197
1678
  this.rotate = !!this._map.options.touchRotate;
1198
1679
  this.zoom = !!this._map.options.touchZoom;
1199
1680
  },
1681
+ /**
1682
+ * Listen for the start of two-finger gestures on the map container.
1683
+ *
1684
+ * @since leaflet-rotate (v0.1.0)
1685
+ */
1200
1686
  addHooks() {
1201
1687
  L.DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this);
1202
1688
  },
1689
+ /**
1690
+ * Remove the touch-start listener.
1691
+ *
1692
+ * @since leaflet-rotate (v0.1.0)
1693
+ */
1203
1694
  removeHooks() {
1204
1695
  L.DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this);
1205
1696
  },
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
+ */
1206
1704
  _onTouchStart(e) {
1207
1705
  const map = this._map;
1208
1706
  if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating) {
@@ -1236,6 +1734,13 @@ var leafletRotate = (() => {
1236
1734
  L.DomEvent.on(document, "touchmove", this._onTouchMove, this).on(document, "touchend touchcancel", this._onTouchEnd, this);
1237
1735
  L.DomEvent.preventDefault(e);
1238
1736
  },
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
+ */
1239
1744
  _onTouchMove(e) {
1240
1745
  if (!e.touches || e.touches.length !== 2 || !(this._zooming || this._rotating)) {
1241
1746
  return;
@@ -1267,8 +1772,8 @@ var leafletRotate = (() => {
1267
1772
  if (scale === 1 && delta.x === 0 && delta.y === 0) {
1268
1773
  return;
1269
1774
  }
1270
- const alpha = -map.getBearing() * (Math.PI / 180);
1271
- 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));
1272
1777
  }
1273
1778
  }
1274
1779
  if (!this._moved) {
@@ -1286,6 +1791,11 @@ var leafletRotate = (() => {
1286
1791
  }
1287
1792
  L.DomEvent.preventDefault(e);
1288
1793
  },
1794
+ /**
1795
+ * Finish the gesture, settle the zoom and remove document listeners.
1796
+ *
1797
+ * @since leaflet-rotate (v0.1.0)
1798
+ */
1289
1799
  _onTouchEnd() {
1290
1800
  if (!this._moved || !(this._zooming || this._rotating)) {
1291
1801
  this._zooming = false;
@@ -1311,14 +1821,26 @@ var leafletRotate = (() => {
1311
1821
  * Whether the map can be rotated with a two-finger rotation gesture
1312
1822
  *
1313
1823
  * @type {Boolean}
1824
+ *
1825
+ * @since leaflet-rotate (v0.1.0)
1314
1826
  */
1315
1827
  touchRotate: false
1316
1828
  });
1317
1829
  L.Map.TouchRotate = L.Handler.extend({
1830
+ /**
1831
+ * Enable rotation handling in the map's shared touch gesture handler.
1832
+ *
1833
+ * @since leaflet-rotate (v0.1.0)
1834
+ */
1318
1835
  addHooks() {
1319
1836
  this._map.touchGestures.enable();
1320
1837
  this._map.touchGestures.rotate = true;
1321
1838
  },
1839
+ /**
1840
+ * Disable rotation handling without affecting pinch zoom.
1841
+ *
1842
+ * @since leaflet-rotate (v0.1.0)
1843
+ */
1322
1844
  removeHooks() {
1323
1845
  this._map.touchGestures.rotate = false;
1324
1846
  }
@@ -1328,33 +1850,170 @@ var leafletRotate = (() => {
1328
1850
  // src/map/handler/ShiftKeyRotate.js
1329
1851
  L.Map.mergeOptions({
1330
1852
  /**
1331
- * Whether the map can be rotated with shift + wheel scroll
1853
+ * Whether Shift+wheel changes the map bearing.
1854
+ *
1332
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
+ *
1863
+ * @type {Boolean}
1864
+ *
1865
+ * @since leaflet-rotate (0.5.0)
1333
1866
  */
1334
- shiftKeyRotate: true
1867
+ pitchDrag: true
1335
1868
  });
1336
1869
  L.Map.ShiftKeyRotate = L.Handler.extend({
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);
1880
+ },
1881
+ /**
1882
+ * Attach enabled wheel and pointer/mouse gesture listeners.
1883
+ *
1884
+ * @since leaflet-rotate (v0.1.0)
1885
+ */
1337
1886
  addHooks() {
1338
- L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
1339
- this._map.shiftKeyRotate.rotate = true;
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
+ }
1340
1893
  },
1894
+ /**
1895
+ * Detach gesture listeners and cancel any active drag.
1896
+ *
1897
+ * @since leaflet-rotate (v0.1.0)
1898
+ */
1341
1899
  removeHooks() {
1342
1900
  L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this);
1343
- 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;
1344
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;
1929
+ },
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
+ */
1345
1937
  _handleShiftScroll(e) {
1346
1938
  if (e.shiftKey) {
1347
1939
  e.preventDefault();
1348
1940
  this._map.scrollWheelZoom.disable();
1349
- this._map.setBearing(this._map._bearing * (180 / Math.PI) + Math.sign(e.deltaY) * 5);
1941
+ this._map.setBearing(this._map.getBearing() + Math.sign(e.deltaY) * 5);
1350
1942
  } else {
1351
1943
  this._map.scrollWheelZoom.enable();
1352
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;
1353
2005
  }
1354
2006
  });
1355
- L.Map.addInitHook("addHandler", "shiftKeyRotate", L.Map.ShiftKeyRotate);
1356
2007
  L.Map.addInitHook(function() {
1357
- 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) {
1358
2017
  this.scrollWheelZoom.disable();
1359
2018
  this.scrollWheelZoom.enable();
1360
2019
  }
@@ -1370,6 +2029,8 @@ var leafletRotate = (() => {
1370
2029
  * for touch-capable web browsers.
1371
2030
  *
1372
2031
  * @type {(Boolean|String)}
2032
+ *
2033
+ * @since leaflet-rotate (v0.1.0)
1373
2034
  */
1374
2035
  touchZoom: L.Browser.touch,
1375
2036
  /**
@@ -1380,15 +2041,27 @@ var leafletRotate = (() => {
1380
2041
  * when pinch-zooming.
1381
2042
  *
1382
2043
  * @type {Boolean}
2044
+ *
2045
+ * @since leaflet-rotate (v0.1.0)
1383
2046
  */
1384
2047
  bounceAtZoomLimits: false
1385
2048
  });
1386
2049
  L.Map.TouchZoom = L.Handler.extend({
2050
+ /**
2051
+ * Enable pinch zoom handling and mark the shared touch handler as active.
2052
+ *
2053
+ * @since leaflet-rotate (v0.1.0)
2054
+ */
1387
2055
  addHooks() {
1388
2056
  this._map._container.classList.add("leaflet-touch-zoom");
1389
2057
  this._map.touchGestures.enable();
1390
2058
  this._map.touchGestures.zoom = true;
1391
2059
  },
2060
+ /**
2061
+ * Disable pinch zoom handling while preserving touch rotation state.
2062
+ *
2063
+ * @since leaflet-rotate (v0.1.0)
2064
+ */
1392
2065
  removeHooks() {
1393
2066
  this._map._container.classList.remove("leaflet-touch-zoom");
1394
2067
  this._map.touchGestures.zoom = false;
@@ -1402,6 +2075,15 @@ var leafletRotate = (() => {
1402
2075
  position: "topleft",
1403
2076
  closeOnZeroBearing: true
1404
2077
  },
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
+ */
1405
2087
  onAdd(map) {
1406
2088
  const container = this._container = L.DomUtil.create("div", "leaflet-control-rotate leaflet-bar");
1407
2089
  const arrow = this._arrow = L.DomUtil.create("span", "leaflet-control-rotate-arrow");
@@ -1426,9 +2108,23 @@ var leafletRotate = (() => {
1426
2108
  }
1427
2109
  return container;
1428
2110
  },
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
+ */
1429
2118
  onRemove(map) {
1430
2119
  map.off("rotate", this._restyle, this);
1431
2120
  },
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
+ */
1432
2128
  _handleMouseDown(e) {
1433
2129
  L.DomEvent.stop(e);
1434
2130
  this.dragging = true;
@@ -1436,11 +2132,25 @@ var leafletRotate = (() => {
1436
2132
  this.dragstartY = e.pageY;
1437
2133
  L.DomEvent.on(document, "mousemove", this._handleMouseDrag, this).on(document, "mouseup", this._handleMouseUp, this);
1438
2134
  },
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
+ */
1439
2142
  _handleMouseUp(e) {
1440
2143
  L.DomEvent.stop(e);
1441
2144
  this.dragging = false;
1442
2145
  L.DomEvent.off(document, "mousemove", this._handleMouseDrag, this).off(document, "mouseup", this._handleMouseUp, this);
1443
2146
  },
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
+ */
1444
2154
  _handleMouseDrag(e) {
1445
2155
  if (!this.dragging) {
1446
2156
  return;
@@ -1448,6 +2158,14 @@ var leafletRotate = (() => {
1448
2158
  const deltaX = e.clientX - this.dragstartX;
1449
2159
  this._map.setBearing(deltaX);
1450
2160
  },
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
+ */
1451
2169
  _cycleState(ev) {
1452
2170
  if (!this._map) {
1453
2171
  return;
@@ -1467,6 +2185,13 @@ var leafletRotate = (() => {
1467
2185
  }
1468
2186
  this._restyle();
1469
2187
  },
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
+ */
1470
2195
  _restyle() {
1471
2196
  if (!this._map.options.rotate) {
1472
2197
  this._link.classList.add("leaflet-disabled");
@@ -1494,6 +2219,14 @@ var leafletRotate = (() => {
1494
2219
  L.control.rotate = (options) => new L.Control.Rotate(options);
1495
2220
  }
1496
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
+ */
1497
2230
  rotateControl: true
1498
2231
  });
1499
2232
  L.Map.addInitHook(function() {