leaflet-rotate 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,13 +30,41 @@ L.Renderer.include({
30
30
  },
31
31
 
32
32
  /**
33
- * Reproject vector paths and refresh renderer bounds after bearing changes.
33
+ * Refresh renderer bounds after bearing or pitch changes.
34
+ *
35
+ * Paths are reprojected only when the pixel origin actually changed (eg. fractional centers
36
+ * after an off-center zoom), since reprojection is the main cost with long polylines.
34
37
  *
35
38
  * @since leaflet-rotate (v0.1.0)
36
39
  */
37
40
  _onRotate() {
38
41
  if (!this._map._rotate) { return; }
39
- this._onZoomEnd();
42
+ const origin = this._map.getPixelOrigin();
43
+ if (!this._rotateOrigin || !this._rotateOrigin.equals(origin)) {
44
+ this._rotateOrigin = origin;
45
+ clearTimeout(this._rotateTimer);
46
+ this._onZoomEnd();
47
+ this._updateAfterRotate();
48
+ return;
49
+ }
50
+ // paths are still aligned: only redraw when the view leaves the already drawn area
51
+ if (this._bounds && this._bounds.contains(this._map._getPaddedPixelBounds(0))) { return; }
52
+ clearTimeout(this._rotateTimer);
53
+ const wait = (this._lastRotateUpdate || 0) + 100 - performance.now();
54
+ if (wait > 0) {
55
+ this._rotateTimer = setTimeout(() => this._map && this._updateAfterRotate(), wait);
56
+ } else {
57
+ this._updateAfterRotate();
58
+ }
59
+ },
60
+
61
+ /**
62
+ * Recalculate renderer bounds and redraw after bearing or pitch changes.
63
+ *
64
+ * @since leaflet-rotate (0.5.1)
65
+ */
66
+ _updateAfterRotate() {
67
+ this._lastRotateUpdate = performance.now();
40
68
  if (L.version.startsWith('2.')) {
41
69
  this._onMoveEnd();
42
70
  } else {
@@ -63,57 +91,31 @@ L.Renderer.include({
63
91
 
64
92
  /**
65
93
  * Update the renderer transform during zoom animation, accounting for rotated coordinates.
66
- *
67
- * @FIXME layer drifts on `map.setZoom()` (eg. zoom during animation)
68
- *
69
- * the main cause seems to be related to `this._updateTransform(path._center, path._zoom))`
70
- * and `this._topLeft = this._map.layerPointToLatLng(this._bounds.min);`
71
- *
72
- * @example
73
- * map.setZoom(2);
74
- * path._renderer._update();
75
- * path._renderer._updateTransform(path._renderer._center, path._renderer._zoom);
76
94
  *
77
95
  * @see https://github.com/Leaflet/Leaflet/pull/8794
78
96
  * @see https://github.com/Leaflet/Leaflet/pull/8103
79
97
  * @see https://github.com/Leaflet/Leaflet/issues/7466
80
98
  *
81
- * @TODO recheck these changes against leaflet@v1.9.3
82
- *
83
- * @see https://github.com/Leaflet/Leaflet/compare/v1.7.0...v1.9.3
84
- *
85
99
  * @param {L.LatLng} center Target map center.
86
100
  * @param {number} zoom Target zoom level.
87
101
  *
88
102
  * @since leaflet-rotate (v0.1.0)
89
103
  */
90
104
  _updateTransform(center, zoom) {
91
- if (L.version.startsWith('2.')) {
92
- if (this._map._rotate) {
93
- const scale = this._map.getZoomScale(zoom, this._zoom),
94
- offset = this._topLeftPoint.multiplyBy(scale)
95
- .subtract(this._map._getNewPixelOrigin(center, zoom));
96
-
97
- L.DomUtil.setTransform(this._container, offset, scale);
98
- return;
99
- }
100
- return rendererBase._updateTransform.apply(this, arguments);
101
- }
105
+ const v2 = L.version.startsWith('2.');
102
106
  if (!this._map._rotate) {
103
- return rendererProto._updateTransform.apply(this, arguments);
107
+ return (v2 ? rendererBase : rendererProto)._updateTransform.apply(this, arguments);
104
108
  }
105
- if (zoom === this._zoom) {
109
+ if (!v2 && zoom === this._zoom) {
106
110
  // _reset() has already repositioned the renderer at the current zoom.
107
111
  return;
108
112
  }
109
- /**
110
- * @FIXME see path._renderer._reset();
111
- */
113
+ // Stay in pixel space: round-tripping `_topLeft` through lat/lng clamps at low zooms (layers jump).
112
114
  const scale = this._map.getZoomScale(zoom, this._zoom),
113
- offset = this._map._latLngToNewLayerPoint(this._topLeft, zoom, center);
115
+ offset = this._topLeftPoint.multiplyBy(scale)
116
+ .subtract(this._map._getNewPixelOrigin(center, zoom));
114
117
 
115
118
  L.DomUtil.setTransform(this._container, offset, scale);
116
-
117
119
  },
118
120
 
119
121
  /**
@@ -211,6 +213,7 @@ L.Renderer.include({
211
213
  this._topLeft = this._map.layerPointToLatLng(this._bounds.min);
212
214
  this._center = this._map.getCenter();
213
215
  this._zoom = this._map.getZoom();
216
+ this._topLeftPoint = this._map._getNewPixelOrigin(this._center, this._zoom).add(this._bounds.min);
214
217
  },
215
218
 
216
219
  });
package/src/map/Map.js CHANGED
@@ -47,6 +47,15 @@ L.Map.mergeOptions({
47
47
  */
48
48
  pitch: 0,
49
49
 
50
+ /**
51
+ * @type {number} Maximum distance of the pitched map plane from the map center, in multiples
52
+ * of the map height. Tiles, vectors and `getBounds()` stop there (and always
53
+ * before the horizon); a `.leaflet-pitch-sky` element fills the area above.
54
+ *
55
+ * @since leaflet-rotate (0.5.1)
56
+ */
57
+ pitchMaxDistance: 3,
58
+
50
59
  });
51
60
 
52
61
  L.Map.include({
@@ -86,6 +95,7 @@ L.Map.include({
86
95
  mapProto.initialize.apply(this, arguments);
87
96
  if (this.options.rotate) {
88
97
  this.on('resize', this._updatePitchTransform, this);
98
+ this.on('move', this._updatePitchSky, this);
89
99
  this.setBearing(this.options.bearing);
90
100
  this.setPitch(this.options.pitch);
91
101
  }
@@ -253,15 +263,66 @@ L.Map.include({
253
263
 
254
264
  // LatLngBounds' constructor automatically
255
265
  // extends the bounds to fit the passed points
256
- const size = this.getSize();
266
+ const [min, max] = this._getPitchedContainerBounds(0);
257
267
  return new L.LatLngBounds([
258
- this.containerPointToLatLng([0, 0]), // topleft
259
- this.containerPointToLatLng([size.x, 0]), // topright
260
- this.containerPointToLatLng([size.x, size.y]), // bottomright
261
- this.containerPointToLatLng([0, size.y]), // bottomleft
268
+ this.containerPointToLatLng([min.x, min.y]), // topleft
269
+ this.containerPointToLatLng([max.x, min.y]), // topright
270
+ this.containerPointToLatLng([max.x, max.y]), // bottomright
271
+ this.containerPointToLatLng([min.x, max.y]), // bottomleft
262
272
  ]);
263
273
  },
264
274
 
275
+ /**
276
+ * Return the container rectangle covering the visible map plane, optionally padded by a
277
+ * fraction of the map size. When pitched, its top is cut at the far plane (see `pitchMaxDistance`)
278
+ * so that it never reaches the horizon, where the plane would extend to infinity.
279
+ *
280
+ * @param {number} padding Padding as a fraction of the map size.
281
+ *
282
+ * @returns {L.Point[]} Top-left and bottom-right container points.
283
+ *
284
+ * @since leaflet-rotate (0.5.1)
285
+ */
286
+ _getPitchedContainerBounds(padding) {
287
+ const size = this.getSize(),
288
+ min = size.multiplyBy(-padding),
289
+ max = size.multiplyBy(1 + padding);
290
+ if (this._pitch) {
291
+ min.y = Math.min(Math.max(min.y, this._getPitchFarY()), max.y);
292
+ }
293
+ return [min, max];
294
+ },
295
+
296
+ /**
297
+ * Return the container y coordinate of the far plane: points above it are farther than
298
+ * `pitchMaxDistance` from the map center, or beyond the horizon.
299
+ *
300
+ * @returns {number} Container y coordinate (`-Infinity` when the whole map is close enough).
301
+ *
302
+ * @since leaflet-rotate (0.5.1)
303
+ */
304
+ _getPitchFarY() {
305
+ const size = this.getSize(),
306
+ inverse = this._pitchInverseTransform,
307
+ panePos = this._getMapPanePos(),
308
+ x = size.x / 2 - panePos.x,
309
+ centerY = size.y / 2 - panePos.y,
310
+ maxDistance = this.options.pitchMaxDistance * size.y,
311
+ w = y => inverse.g * x + inverse.h * y + inverse.i,
312
+ centerW = w(centerY),
313
+ center = this._transformPitchPoint([x, centerY], inverse),
314
+ // the horizon is horizontal on screen (bearing is applied before the pitch)
315
+ isNear = y => w(y) * centerW > 0 && this._transformPitchPoint([x, y], inverse).distanceTo(center) <= maxDistance;
316
+
317
+ let far = -size.y - panePos.y, near = centerY;
318
+ if (isNear(far)) { return -Infinity; }
319
+ for (let i = 0; i < 20; i++) {
320
+ const mid = (far + near) / 2;
321
+ if (isNear(mid)) { near = mid; } else { far = mid; }
322
+ }
323
+ return near + panePos.y;
324
+ },
325
+
265
326
  /**
266
327
  * Returns the bounds of the current map view in projected pixel
267
328
  * coordinates (sometimes useful in layer and overlay implementations).
@@ -298,48 +359,110 @@ L.Map.include({
298
359
  if (!this._rotate) { return; }
299
360
 
300
361
  if (options && options.animate) {
301
- this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
302
- const animationId = this._bearingAnimationId;
303
- if (this._bearingAnimationFrame) {
304
- cancelAnimationFrame(this._bearingAnimationFrame);
305
- }
306
-
307
- const startBearing = this.getBearing(),
308
- endBearing = L.Util.wrapNum(theta, [0, 360]),
309
- delta = ((endBearing - startBearing + 540) % 360) - 180,
310
- duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1000,
311
- easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2),
312
- startTime = performance.now();
313
-
314
- const animate = function() {
315
- if (!this._mapPane) {
316
- delete this._bearingAnimationFrame;
317
- return;
318
- }
319
-
320
- const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1,
321
- eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
322
-
323
- this._setBearing(progress === 1 ? endBearing : startBearing + delta * eased);
324
- if (this._bearingAnimationId !== animationId) { return; }
325
- if (progress < 1) {
326
- this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
327
- } else {
328
- delete this._bearingAnimationFrame;
329
- }
330
- };
331
-
332
- this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
362
+ this._animatePitchBearing(this.getPitch(), theta, options);
333
363
  return;
334
364
  }
335
365
 
366
+ this._stopPitchBearingAnimation();
367
+ this._setBearing(theta);
368
+ },
369
+
370
+ /**
371
+ * Animate pitch and bearing together (bearing follows the shortest path).
372
+ *
373
+ * @param {number} pitch Target pitch in degrees.
374
+ * @param {number} bearing Target bearing in degrees.
375
+ * @param {Object} [options] Animation options.
376
+ * @param {number} [options.duration=0.25] Animation duration in seconds.
377
+ * @param {number} [options.easeLinearity=0.5] Easing curve linearity.
378
+ *
379
+ * @since leaflet-rotate (0.5.1)
380
+ */
381
+ _animatePitchBearing(pitch, bearing, options) {
382
+ this._stopPitchBearingAnimation();
383
+ const animationId = this._bearingAnimationId;
384
+
385
+ const startBearing = this.getBearing(),
386
+ startPitch = this.getPitch(),
387
+ endBearing = L.Util.wrapNum(bearing, [0, 360]),
388
+ endPitch = Math.max(0, Math.min(pitch, 85)),
389
+ delta = ((endBearing - startBearing + 540) % 360) - 180,
390
+ duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1000,
391
+ easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2),
392
+ startTime = performance.now();
393
+
394
+ const animate = function() {
395
+ if (!this._mapPane || this._bearingAnimationId !== animationId) {
396
+ return;
397
+ }
398
+
399
+ const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1,
400
+ eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
401
+
402
+ if (progress < 1) {
403
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
404
+ } else {
405
+ delete this._bearingAnimationFrame;
406
+ }
407
+ this._setPitchAndBearing(
408
+ progress === 1 ? endPitch : startPitch + (endPitch - startPitch) * eased,
409
+ progress === 1 ? endBearing : startBearing + delta * eased
410
+ );
411
+ };
412
+
413
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
414
+ },
415
+
416
+ /**
417
+ * Cancel a running pitch/bearing animation.
418
+ *
419
+ * @since leaflet-rotate (0.5.1)
420
+ */
421
+ _stopPitchBearingAnimation() {
336
422
  this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
337
423
  if (this._bearingAnimationFrame) {
338
424
  cancelAnimationFrame(this._bearingAnimationFrame);
339
425
  delete this._bearingAnimationFrame;
340
426
  }
427
+ },
341
428
 
342
- this._setBearing(theta);
429
+ /**
430
+ * Rotate the map back to north (bearing 0), animated by default.
431
+ *
432
+ * @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
433
+ *
434
+ * @returns {L.Map} This map instance.
435
+ *
436
+ * @since leaflet-rotate (0.5.1)
437
+ */
438
+ resetNorth(options) {
439
+ options = Object.assign({ animate: true, duration: 0.5 }, options);
440
+ if (options.animate) {
441
+ this._animatePitchBearing(this.getPitch(), 0, options);
442
+ } else {
443
+ this.setBearing(0);
444
+ }
445
+ return this;
446
+ },
447
+
448
+ /**
449
+ * Rotate the map back to north and remove the pitch, animated by default.
450
+ *
451
+ * @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
452
+ *
453
+ * @returns {L.Map} This map instance.
454
+ *
455
+ * @since leaflet-rotate (0.5.1)
456
+ */
457
+ resetNorthPitch(options) {
458
+ options = Object.assign({ animate: true, duration: 0.5 }, options);
459
+ if (options.animate) {
460
+ this._animatePitchBearing(0, 0, options);
461
+ } else {
462
+ this._stopPitchBearingAnimation();
463
+ this._setPitchAndBearing(0, 0);
464
+ }
465
+ return this;
343
466
  },
344
467
 
345
468
  /**
@@ -422,7 +545,8 @@ L.Map.include({
422
545
 
423
546
  this._rotatePane.style.transformOrigin = '0 0';
424
547
  this._rotatePane.style.transform = transform;
425
- const matrix = this._pitchTransform = new DOMMatrix(getComputedStyle(this._rotatePane).transform),
548
+ // parse the transform directly: getComputedStyle() would force a style recalculation on every frame
549
+ const matrix = this._pitchTransform = new DOMMatrix(transform),
426
550
  a = matrix.m11, b = matrix.m21, c = matrix.m41,
427
551
  d = matrix.m12, e = matrix.m22, f = matrix.m42,
428
552
  g = matrix.m14, h = matrix.m24, i = matrix.m44,
@@ -439,12 +563,38 @@ L.Map.include({
439
563
  h: (b * g - a * h) / determinant,
440
564
  i: (a * e - b * d) / determinant,
441
565
  };
566
+ this._updatePitchSky();
567
+ },
568
+
569
+ /**
570
+ * Fill the area above the far plane (where no tiles are loaded) with a sky gradient.
571
+ * Style it through the `.leaflet-pitch-sky` class.
572
+ *
573
+ * @since leaflet-rotate (0.5.1)
574
+ */
575
+ _updatePitchSky() {
576
+ const farY = this._pitch && this._pitchInverseTransform ? this._getPitchFarY() : -Infinity;
577
+ if (farY <= 0) {
578
+ if (this._pitchSky) { this._pitchSky.style.display = 'none'; }
579
+ return;
580
+ }
581
+ if (!this._pitchSky) {
582
+ this._pitchSky = L.DomUtil.create('div', 'leaflet-pitch-sky', this._container);
583
+ Object.assign(this._pitchSky.style, {
584
+ position: 'absolute', left: 0, top: 0, width: '100%', zIndex: 0, pointerEvents: 'none',
585
+ background: 'linear-gradient(#9ec5e8, #dfe9f0)',
586
+ });
587
+ }
588
+ this._pitchSky.style.display = '';
589
+ // overlap the far edge slightly to hide antialiasing gaps
590
+ this._pitchSky.style.height = `${Math.ceil(farY) + 1}px`;
442
591
  },
443
592
 
444
593
  /**
445
594
  * Set the map pitch in degrees. Values are clamped to the range 0–85.
446
595
  *
447
596
  * @param {number} pitch Pitch angle in degrees; clamped to [0, 85].
597
+ * @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
448
598
  *
449
599
  * @returns {L.Map} This map instance.
450
600
  *
@@ -452,7 +602,12 @@ L.Map.include({
452
602
  *
453
603
  * @since leaflet-rotate (0.5.0)
454
604
  */
455
- setPitch(pitch) {
605
+ setPitch(pitch, options) {
606
+ if (options && options.animate) {
607
+ this._animatePitchBearing(pitch, this.getBearing(), options);
608
+ return this;
609
+ }
610
+ this._stopPitchBearingAnimation();
456
611
  this._setPitchAndBearing(pitch, this.getBearing());
457
612
  return this;
458
613
  },
@@ -484,6 +639,7 @@ L.Map.include({
484
639
  }
485
640
  this._setBearing(bearing, mapCenter);
486
641
  if (pitchChanged) {
642
+ if (!this._pitch) { this._updatePitchSky(); }
487
643
  this.fire('pitch');
488
644
  }
489
645
  },
@@ -499,6 +655,38 @@ L.Map.include({
499
655
  return (this._pitch || 0) * (180 / Math.PI);
500
656
  },
501
657
 
658
+ /**
659
+ * Zoom while keeping the given point fixed on screen, also when the map is pitched.
660
+ *
661
+ * Leaflet interpolates in container space, which is not linear under perspective;
662
+ * interpolating in layer space keeps the point under the cursor.
663
+ *
664
+ * @param {L.LatLng|L.Point} latlng Geographic point or container point to keep fixed.
665
+ * @param {number} zoom Target zoom level.
666
+ * @param {Object} [options] Zoom options.
667
+ *
668
+ * @returns {L.Map} This map instance.
669
+ *
670
+ * @since leaflet-rotate (0.5.1)
671
+ */
672
+ setZoomAround(latlng, zoom, options) {
673
+ if (!this._pitch) {
674
+ return mapProto.setZoomAround.apply(this, arguments);
675
+ }
676
+ const scale = this.getZoomScale(zoom),
677
+ size = this.getSize(),
678
+ center = this.containerPointToLayerPoint(size.divideBy(2)),
679
+ containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng),
680
+ point = this.containerPointToLayerPoint(containerPoint),
681
+ newCenter = this.layerPointToLatLng(center.add(point.subtract(center).multiplyBy(1 - 1 / scale)));
682
+ options = Object.assign({}, options);
683
+ // Leaflet skips the animation when the origin is "far" in layer space, which is always the case near the horizon.
684
+ if (options.animate === undefined && new L.Bounds(new L.Point(0, 0), size).contains(containerPoint)) {
685
+ options.animate = true;
686
+ }
687
+ return this.setView(newCenter, zoom, { zoom: options });
688
+ },
689
+
502
690
  /**
503
691
  * Get the current map bearing. Enable the `rotate` map option before calling.
504
692
  *
@@ -782,20 +970,15 @@ L.Map.include({
782
970
  *
783
971
  * @param {L.LatLng} latlng Geographic point.
784
972
  *
785
- * @returns {L.Point} Offset in layer pixels, adjusted for bearing and pitch.
973
+ * @returns {L.Point} Offset in container pixels (the map pane is panned in screen space), adjusted for bearing and pitch.
786
974
  *
787
975
  * @since leaflet-rotate (v0.1.0)
788
976
  */
789
977
  _getCenterOffset(latlng) {
790
- let centerOffset = mapProto._getCenterOffset.apply(this, arguments);
791
- if (this._pitch) {
792
- const center = this.getSize()._divideBy(2);
793
- centerOffset = this.containerPointToLayerPoint(center.add(centerOffset))
794
- .subtract(this.containerPointToLayerPoint(center));
795
- } else if (this._rotate) {
796
- centerOffset = centerOffset.rotate(this._bearing);
978
+ if (!this._rotate) {
979
+ return mapProto._getCenterOffset.apply(this, arguments);
797
980
  }
798
- return centerOffset;
981
+ return this.latLngToContainerPoint(latlng)._subtract(this.getSize()._divideBy(2));
799
982
  },
800
983
 
801
984
  /**
@@ -826,16 +1009,13 @@ L.Map.include({
826
1009
  * @since leaflet-rotate (v0.1.0)
827
1010
  */
828
1011
  _getNewPixelOrigin(center, zoom) {
829
- if (!this._rotate || L.version.startsWith('2.')) {
1012
+ if (!this._rotate) {
830
1013
  return mapProto._getNewPixelOrigin.apply(this, arguments);
831
1014
  }
832
- const viewHalf = this.getSize()._divideBy(2);
1015
+ // the layer point under the view center doesn't depend on zoom (pane transforms only rotate/pitch around the pivot),
1016
+ // so this matches `_setBearing` also when the pivot is no longer the view center (eg. after panning)
833
1017
  return this.project(center, zoom)
834
- .rotate(this._bearing)
835
- ._subtract(viewHalf)
836
- ._add(this._getMapPanePos())
837
- ._add(this._getRotatePanePos())
838
- .rotate(-this._bearing)
1018
+ ._subtract(this.containerPointToLayerPoint(this.getSize()._divideBy(2)))
839
1019
  ._round();
840
1020
  },
841
1021
 
@@ -860,13 +1040,17 @@ L.Map.include({
860
1040
  const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(),
861
1041
  scale = this.getZoomScale(mapZoom, zoom),
862
1042
  pixelCenter = this.project(center, zoom).floor(),
863
- size = this.getSize(),
864
- halfSize = new L.Bounds([
865
- this.containerPointToLayerPoint([0, 0]).floor(),
866
- this.containerPointToLayerPoint([size.x, 0]).floor(),
867
- this.containerPointToLayerPoint([0, size.y]).floor(),
868
- this.containerPointToLayerPoint([size.x, size.y]).floor()
869
- ]).getSize().divideBy(scale * 2);
1043
+ [min, max] = this._getPitchedContainerBounds(0),
1044
+ corners = [[min.x, min.y], [max.x, min.y], [min.x, max.y], [max.x, max.y]]
1045
+ .map(point => this.containerPointToLayerPoint(point));
1046
+
1047
+ if (this._pitch) {
1048
+ // the pitched view is not symmetric: the far side covers much more than the near one
1049
+ const viewCenter = this.containerPointToLayerPoint(this.getSize()._divideBy(2));
1050
+ return new L.Bounds(corners.map(point => pixelCenter.add(point.subtract(viewCenter).divideBy(scale))));
1051
+ }
1052
+
1053
+ const halfSize = new L.Bounds(corners.map(point => point.floor())).getSize().divideBy(scale * 2);
870
1054
 
871
1055
  return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
872
1056
  },
@@ -896,11 +1080,7 @@ L.Map.include({
896
1080
  if (!this._rotate && mapProto._getPaddedPixelBounds) {
897
1081
  return mapProto._getPaddedPixelBounds.apply(this, arguments);
898
1082
  }
899
- const p = padding,
900
- size = this.getSize(),
901
- padMin = size.multiplyBy(-p),
902
- padMax = size.multiplyBy(1 + p);
903
- //min = this.containerPointToLayerPoint(size.multiplyBy(-p)).round();
1083
+ const [padMin, padMax] = this._getPitchedContainerBounds(padding);
904
1084
 
905
1085
  return new L.Bounds([
906
1086
  this.containerPointToLayerPoint([padMin.x, padMin.y]).floor(),
@@ -27,12 +27,19 @@ L.Map.CompassBearing = L.Handler.extend({
27
27
 
28
28
  /**
29
29
  * Subscribe to the supported device-orientation event, if available.
30
+ * On iOS, permission is requested first (call `enable()` from a user gesture).
30
31
  *
31
32
  * @since leaflet-rotate (v0.1.0)
32
33
  */
33
34
  addHooks() {
34
35
  if (this._map._rotate && this.__deviceOrientationEvent) {
35
36
  L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this);
37
+ const DOE = window.DeviceOrientationEvent;
38
+ if (DOE && DOE.requestPermission) {
39
+ DOE.requestPermission()
40
+ .then(state => 'denied' === state && this.enabled() && this.disable())
41
+ .catch(() => {});
42
+ }
36
43
  } else {
37
44
  // L.Map.CompassBearing handler will be automatically
38
45
  // disabled if device orientation is not supported.