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
package/src/map/Map.js CHANGED
@@ -1,16 +1,65 @@
1
1
  /**
2
+ * Extends Leaflet maps with bearing rotation and 3D pitch.
3
+ *
2
4
  * @external L.Map
3
5
  *
6
+ * @since leaflet-rotate (v0.1.0)
7
+ *
4
8
  * @see https://github.com/Leaflet/Leaflet/blob/v1.9.3/src/map/Map.js
5
9
  */
6
10
 
7
- const mapProto = L.extend({}, L.Map.prototype);
11
+ /**
12
+ * @event L.Map#rotate Fired after the map bearing changes.
13
+ *
14
+ * @since leaflet-rotate (v0.1.0)
15
+ */
16
+
17
+ /**
18
+ * @event L.Map#pitch Fired after the map pitch changes.
19
+ *
20
+ * @since leaflet-rotate (0.5.0)
21
+ */
22
+
23
+ import { toPoint } from '../geometry/Point';
24
+
25
+ const mapProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Map.prototype));
26
+
27
+ L.Map.mergeOptions({
28
+
29
+ /**
30
+ * @type {boolean} Enable bearing and pitch transformations.
31
+ *
32
+ * @since leaflet-rotate (v0.1.0)
33
+ */
34
+ rotate: false,
35
+
36
+ /**
37
+ * @type {number} Initial map bearing in degrees.
38
+ *
39
+ * @since leaflet-rotate (v0.1.0)
40
+ */
41
+ bearing: 0,
42
+
43
+ /**
44
+ * @type {number} Initial map pitch in degrees, clamped to 0–85.
45
+ *
46
+ * @since leaflet-rotate (0.5.0)
47
+ */
48
+ pitch: 0,
8
49
 
9
- L.Map.mergeOptions({ rotate: false, bearing: 0, });
50
+ });
10
51
 
11
52
  L.Map.include({
12
53
 
13
- _animateZoomNoDelay: function(center, zoom) {
54
+ /**
55
+ * Apply an immediate animated-zoom frame without waiting for a transition delay.
56
+ *
57
+ * @param {L.LatLng} center Target center.
58
+ * @param {number} zoom Target zoom level.
59
+ *
60
+ * @since leaflet-rotate (v0.1.0)
61
+ */
62
+ _animateZoomNoDelay(center, zoom) {
14
63
  if (!this._mapPane) { return; }
15
64
 
16
65
  this._animatingZoom = true;
@@ -21,17 +70,24 @@ L.Map.include({
21
70
  },
22
71
 
23
72
  /**
24
- * @param {(HTMLElement|String)} id html selector
25
- * @param {Object} [options={}] leaflet map options
73
+ * Initialize the Leaflet map and apply its initial bearing and pitch.
74
+ *
75
+ * @param {HTMLElement|string} id Map container element or its ID.
76
+ * @param {Object} [options={}] Leaflet map options, including `rotate`, `bearing`, and `pitch`.
77
+ *
78
+ * @since leaflet-rotate (v0.1.0)
26
79
  */
27
- initialize: function(id, options) {
80
+ initialize(id, options) {
28
81
  if (options.rotate) {
29
82
  this._rotate = true;
30
83
  this._bearing = 0;
84
+ this._pitch = 0;
31
85
  }
32
86
  mapProto.initialize.apply(this, arguments);
33
- if(this.options.rotate){
34
- this.setBearing(this.options.bearing);
87
+ if (this.options.rotate) {
88
+ this.on('resize', this._updatePitchTransform, this);
89
+ this.setBearing(this.options.bearing);
90
+ this.setPitch(this.options.pitch);
35
91
  }
36
92
  },
37
93
 
@@ -40,14 +96,23 @@ L.Map.include({
40
96
  * returns the corresponding pixel coordinate relative to
41
97
  * the [origin pixel](#map-getpixelorigin).
42
98
  *
43
- * @param {L.Point} point pixel screen coordinates
44
- * @returns {L.Point} transformed pixel point
99
+ * @param {L.Point|number[]} point Pixel coordinate in the map container.
100
+ *
101
+ * @returns {L.Point} Corresponding layer-point coordinate.
102
+ *
103
+ * @since leaflet-rotate (v0.1.0)
45
104
  */
46
- containerPointToLayerPoint: function(point) {
105
+ containerPointToLayerPoint(point) {
47
106
  if (!this._rotate) {
48
107
  return mapProto.containerPointToLayerPoint.apply(this, arguments);
49
108
  }
50
- return L.point(point)
109
+ if (this._pitch) {
110
+ return this._transformPitchPoint(
111
+ toPoint(point).subtract(this._getMapPanePos()),
112
+ this._pitchInverseTransform
113
+ );
114
+ }
115
+ return toPoint(point)
51
116
  .subtract(this._getMapPanePos())
52
117
  .rotateFrom(-this._bearing, this._getRotatePanePos())
53
118
  .subtract(this._getRotatePanePos());
@@ -57,14 +122,20 @@ L.Map.include({
57
122
  * Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
58
123
  * returns the corresponding pixel coordinate relative to the map container.
59
124
  *
60
- * @param {L.Point} point pixel screen coordinates
61
- * @returns {L.Point} transformed pixel point
125
+ * @param {L.Point|number[]} point Pixel coordinate in layer-point space.
126
+ *
127
+ * @returns {L.Point} Corresponding pixel coordinate in the map container.
128
+ *
129
+ * @since leaflet-rotate (v0.1.0)
62
130
  */
63
- layerPointToContainerPoint: function(point) {
131
+ layerPointToContainerPoint(point) {
64
132
  if (!this._rotate) {
65
133
  return mapProto.layerPointToContainerPoint.apply(this, arguments);
66
134
  }
67
- return L.point(point)
135
+ if (this._pitch) {
136
+ return this._transformPitchPoint(point, this._pitchTransform).add(this._getMapPanePos());
137
+ }
138
+ return toPoint(point)
68
139
  .add(this._getRotatePanePos())
69
140
  .rotateFrom(this._bearing, this._getRotatePanePos())
70
141
  .add(this._getMapPanePos());
@@ -77,13 +148,17 @@ L.Map.include({
77
148
  * (rotatePane) --> (mapPane)
78
149
  * (rotatePane) --> (norotatePane)
79
150
  *
80
- * @param {L.Point} point pixel screen coordinates
81
- * @returns {L.Point}
151
+ * @param {L.Point|number[]} point Point in the rotated pane.
152
+ *
153
+ * @returns {L.Point} Corresponding point in map-pane coordinates.
82
154
  *
83
- * @since leaflet-rotate (v0.1)
155
+ * @since leaflet-rotate (v0.1.0)
84
156
  */
85
- rotatedPointToMapPanePoint: function(point) {
86
- return L.point(point)
157
+ rotatedPointToMapPanePoint(point) {
158
+ if (this._pitch) {
159
+ return this._transformPitchPoint(point, this._pitchTransform);
160
+ }
161
+ return toPoint(point)
87
162
  .rotate(this._bearing)
88
163
  ._add(this._getRotatePanePos());
89
164
  },
@@ -95,22 +170,27 @@ L.Map.include({
95
170
  * (mapPane) --> (rotatePane)
96
171
  * (norotatePane) --> (rotatePane)
97
172
  *
98
- * @param {L.Point} point pixel screen coordinates
173
+ * @param {L.Point|number[]} point Point in map-pane coordinates.
174
+ *
175
+ * @returns {L.Point} Corresponding point in the rotated pane.
99
176
  *
100
- * @since leaflet-rotate (v0.1)
177
+ * @since leaflet-rotate (v0.1.0)
101
178
  */
102
- mapPanePointToRotatedPoint: function(point) {
103
- return L.point(point)
179
+ mapPanePointToRotatedPoint(point) {
180
+ if (this._pitch) {
181
+ return this._transformPitchPoint(point, this._pitchInverseTransform);
182
+ }
183
+ return toPoint(point)
104
184
  ._subtract(this._getRotatePanePos())
105
185
  .rotate(-this._bearing);
106
186
  },
107
187
 
108
- // latLngToLayerPoint: function (latlng) {
109
- // var projectedPoint = this.project(L.latLng(latlng))._round();
188
+ // latLngToLayerPoint(latlng) {
189
+ // const projectedPoint = this.project(L.latLng(latlng))._round();
110
190
  // return projectedPoint._subtract(this.getPixelOrigin());
111
191
  // },
112
192
 
113
- // latLngToContainerPoint: function (latlng) {
193
+ // latLngToContainerPoint(latlng) {
114
194
  // return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
115
195
  // },
116
196
 
@@ -120,12 +200,13 @@ L.Map.include({
120
200
  *
121
201
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/map/Map.js#L1114-L1135
122
202
  *
123
- * @param {L.LatLngBounds} bounds
124
- * @returns {L.Bounds}
203
+ * @param {L.LatLngBounds} bounds Geographic bounds to project.
125
204
  *
126
- * @since leaflet-rotate (v0.2)
205
+ * @returns {L.Bounds} Axis-aligned bounds in container-pixel coordinates.
206
+ *
207
+ * @since leaflet-rotate (v0.2.0)
127
208
  */
128
- mapBoundsToContainerBounds: function (bounds) {
209
+ mapBoundsToContainerBounds(bounds) {
129
210
  if (!this._rotate && mapProto.mapBoundsToContainerBounds) {
130
211
  return mapProto.mapBoundsToContainerBounds.apply(this, arguments);
131
212
  }
@@ -142,30 +223,29 @@ L.Map.include({
142
223
  sw = this.layerPointToContainerPoint(this.project(bounds.getSouthWest())._subtract(origin)),
143
224
  se = this.layerPointToContainerPoint(this.project(bounds.getSouthEast())._subtract(origin));
144
225
 
145
- return L.bounds([
146
- L.point(Math.min(nw.x, ne.x, se.x, sw.x), Math.min(nw.y, ne.y, se.y, sw.y)), // [ minX, minY ]
147
- L.point(Math.max(nw.x, ne.x, se.x, sw.x), Math.max(nw.y, ne.y, se.y, sw.y)) // [ maxX, maxY ]
226
+ return new L.Bounds([
227
+ new L.Point(Math.min(nw.x, ne.x, se.x, sw.x), Math.min(nw.y, ne.y, se.y, sw.y)), // [ minX, minY ]
228
+ new L.Point(Math.max(nw.x, ne.x, se.x, sw.x), Math.max(nw.y, ne.y, se.y, sw.y)) // [ maxX, maxY ]
148
229
  ]);
149
230
  },
150
231
 
151
232
  /**
152
- * Returns geographical bounds visible in the current map view
233
+ * Return the geographic bounds of the current viewport, accounting for rotation and pitch.
153
234
  *
154
- * @TODO find out if map bounds calculated by `L.Map::getBounds()`
155
- * function should match the `rotatePane` or `norotatePane` bounds
235
+ * The result is the smallest geographic bounds containing all four viewport corners.
156
236
  *
157
- * @see https://github.com/fnicollet/Leaflet/issues/7
237
+ * @returns {L.LatLngBounds} Geographic bounds visible in the map viewport.
158
238
  *
159
- * @returns {L.LatLngBounds}
239
+ * @since leaflet-rotate (v0.1.0)
160
240
  */
161
- getBounds: function() {
241
+ getBounds() {
162
242
  if (!this._rotate) {
163
243
  return mapProto.getBounds.apply(this, arguments);
164
244
  }
165
245
 
166
246
  // SEE: https://github.com/fnicollet/Leaflet/pull/22
167
247
  //
168
- // var bounds = this.getPixelBounds(),
248
+ // const bounds = this.getPixelBounds(),
169
249
  // sw = this.unproject(bounds.getBottomLeft()),
170
250
  // ne = this.unproject(bounds.getTopRight());
171
251
  // return new LatLngBounds(sw, ne);
@@ -173,7 +253,7 @@ L.Map.include({
173
253
 
174
254
  // LatLngBounds' constructor automatically
175
255
  // extends the bounds to fit the passed points
176
- var size = this.getSize();
256
+ const size = this.getSize();
177
257
  return new L.LatLngBounds([
178
258
  this.containerPointToLatLng([0, 0]), // topleft
179
259
  this.containerPointToLatLng([size.x, 0]), // topright
@@ -200,89 +280,234 @@ L.Map.include({
200
280
  // },
201
281
 
202
282
  /**
203
- * Change map rotation
283
+ * Set the map bearing, optionally animating to the new value.
284
+ *
285
+ * @param {number} theta Target bearing in degrees; normalized to [0, 360).
286
+ * @param {Object} [options] Animation options.
287
+ * @param {boolean} [options.animate=false] Animate the change.
288
+ * @param {number} [options.duration=0.25] Animation duration in seconds.
289
+ * @param {number} [options.easeLinearity=0.5] Easing curve linearity.
290
+ *
291
+ * @returns {void}
204
292
  *
205
- * @param {number} theta map degrees
206
- * @param {Object} [options] animation options
207
- * @param {boolean} [options.animate=false]
208
- * @param {number} [options.duration=0.25] animation duration in seconds
209
- * @param {number} [options.easeLinearity=0.5]
293
+ * @fires L.Map#rotate
210
294
  *
211
- * @since leaflet-rotate (v0.1)
295
+ * @since leaflet-rotate (v0.1.0)
212
296
  */
213
- setBearing: function(theta, options) {
214
- if (!L.Browser.any3d || !this._rotate) { return; }
297
+ setBearing(theta, options) {
298
+ if (!this._rotate) { return; }
215
299
 
216
300
  if (options && options.animate) {
217
301
  this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
218
- var animationId = this._bearingAnimationId;
302
+ const animationId = this._bearingAnimationId;
219
303
  if (this._bearingAnimationFrame) {
220
- L.Util.cancelAnimFrame(this._bearingAnimationFrame);
304
+ cancelAnimationFrame(this._bearingAnimationFrame);
221
305
  }
222
306
 
223
- var startBearing = this.getBearing(),
307
+ const startBearing = this.getBearing(),
224
308
  endBearing = L.Util.wrapNum(theta, [0, 360]),
225
309
  delta = ((endBearing - startBearing + 540) % 360) - 180,
226
310
  duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1000,
227
311
  easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2),
228
312
  startTime = performance.now();
229
313
 
230
- var animate = function() {
314
+ const animate = function() {
231
315
  if (!this._mapPane) {
232
316
  delete this._bearingAnimationFrame;
233
317
  return;
234
318
  }
235
319
 
236
- var progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1,
320
+ const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1,
237
321
  eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
238
322
 
239
323
  this._setBearing(progress === 1 ? endBearing : startBearing + delta * eased);
240
324
  if (this._bearingAnimationId !== animationId) { return; }
241
325
  if (progress < 1) {
242
- this._bearingAnimationFrame = L.Util.requestAnimFrame(animate, this);
326
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
243
327
  } else {
244
328
  delete this._bearingAnimationFrame;
245
329
  }
246
330
  };
247
331
 
248
- this._bearingAnimationFrame = L.Util.requestAnimFrame(animate, this);
332
+ this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
249
333
  return;
250
334
  }
251
335
 
252
336
  this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
253
337
  if (this._bearingAnimationFrame) {
254
- L.Util.cancelAnimFrame(this._bearingAnimationFrame);
338
+ cancelAnimationFrame(this._bearingAnimationFrame);
255
339
  delete this._bearingAnimationFrame;
256
340
  }
257
341
 
258
342
  this._setBearing(theta);
259
343
  },
260
344
 
261
- _setBearing: function(theta) {
262
- var bearing = L.Util.wrapNum(theta, [0, 360]) * L.DomUtil.DEG_TO_RAD,
345
+ /**
346
+ * Apply a bearing immediately and update the rotated pane and map coordinates.
347
+ *
348
+ * @param {number} theta Bearing in degrees.
349
+ * @param {L.LatLng} [mapCenter] Center to preserve while applying the change.
350
+ *
351
+ * @fires L.Map#rotate
352
+ *
353
+ * @since leaflet-rotate (v0.1.0)
354
+ */
355
+ _setBearing(theta, mapCenter) {
356
+ if (!mapCenter && this._loaded) {
357
+ mapCenter = this.getCenter();
358
+ }
359
+
360
+ const bearing = L.Util.wrapNum(theta, [0, 360]) * (Math.PI / 180),
263
361
  center = this._getPixelCenter(),
264
362
  oldPos = this._getRotatePanePos().rotateFrom(-this._bearing, center),
265
363
  newPos = oldPos.rotateFrom(bearing, center);
266
364
 
267
- // CSS transform
268
- L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
269
-
270
365
  this._pivot = center;
271
366
  this._bearing = bearing;
272
367
  this._rotatePanePos = newPos;
273
368
 
369
+ if (this._pitch) {
370
+ this._rotatePane._leaflet_pos = oldPos;
371
+ this._updatePitchTransform();
372
+ } else {
373
+ L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
374
+ }
375
+
376
+ if (mapCenter) {
377
+ this._pixelOrigin = this.project(mapCenter, this._zoom)
378
+ .subtract(this.containerPointToLayerPoint(this.getSize().divideBy(2)));
379
+ delete this._lastCenter;
380
+ this.fire('move');
381
+ }
382
+
274
383
  this.fire('rotate');
275
384
  },
276
385
 
277
386
  /**
278
- * Get current map rotation
387
+ * Transform a point with a projective matrix, accepting DOMMatrix or its 2D inverse.
279
388
  *
280
- * @returns {number} theta map degrees
389
+ * @param {L.Point|number[]} point Input pixel coordinate.
390
+ * @param {DOMMatrix|Object} matrix Forward or inverse transformation matrix.
281
391
  *
282
- * @since leaflet-rotate (v0.1)
392
+ * @returns {L.Point} Transformed pixel coordinate.
393
+ *
394
+ * @since leaflet-rotate (0.5.0)
283
395
  */
284
- getBearing: function() {
285
- return this._bearing * L.DomUtil.RAD_TO_DEG;
396
+ _transformPitchPoint(point, matrix) {
397
+ point = toPoint(point);
398
+ if (!(matrix instanceof DOMMatrix)) {
399
+ const denominator = matrix.g * point.x + matrix.h * point.y + matrix.i;
400
+ return new L.Point(
401
+ (matrix.a * point.x + matrix.b * point.y + matrix.c) / denominator,
402
+ (matrix.d * point.x + matrix.e * point.y + matrix.f) / denominator
403
+ );
404
+ }
405
+ const transformed = new DOMPoint(point.x, point.y).matrixTransform(matrix);
406
+ return new L.Point(transformed.x / transformed.w, transformed.y / transformed.w);
407
+ },
408
+
409
+ /**
410
+ * Apply the current pitch/bearing CSS transform and cache its forward and inverse matrices.
411
+ *
412
+ * @since leaflet-rotate (0.5.0)
413
+ */
414
+ _updatePitchTransform() {
415
+ if (!this._pitch || !this._rotatePane) { return; }
416
+
417
+ const pivot = this._getPixelCenter(),
418
+ size = this.getSize(),
419
+ perspective = Math.max(1, size.y * 1.5),
420
+ position = this._rotatePane._leaflet_pos || new L.Point(0, 0),
421
+ 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)`;
422
+
423
+ this._rotatePane.style.transformOrigin = '0 0';
424
+ this._rotatePane.style.transform = transform;
425
+ const matrix = this._pitchTransform = new DOMMatrix(getComputedStyle(this._rotatePane).transform),
426
+ a = matrix.m11, b = matrix.m21, c = matrix.m41,
427
+ d = matrix.m12, e = matrix.m22, f = matrix.m42,
428
+ g = matrix.m14, h = matrix.m24, i = matrix.m44,
429
+ determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
430
+
431
+ this._pitchInverseTransform = {
432
+ a: (e * i - f * h) / determinant,
433
+ b: (c * h - b * i) / determinant,
434
+ c: (b * f - c * e) / determinant,
435
+ d: (f * g - d * i) / determinant,
436
+ e: (a * i - c * g) / determinant,
437
+ f: (c * d - a * f) / determinant,
438
+ g: (d * h - e * g) / determinant,
439
+ h: (b * g - a * h) / determinant,
440
+ i: (a * e - b * d) / determinant,
441
+ };
442
+ },
443
+
444
+ /**
445
+ * Set the map pitch in degrees. Values are clamped to the range 0–85.
446
+ *
447
+ * @param {number} pitch Pitch angle in degrees; clamped to [0, 85].
448
+ *
449
+ * @returns {L.Map} This map instance.
450
+ *
451
+ * @fires L.Map#pitch
452
+ *
453
+ * @since leaflet-rotate (0.5.0)
454
+ */
455
+ setPitch(pitch) {
456
+ this._setPitchAndBearing(pitch, this.getBearing());
457
+ return this;
458
+ },
459
+
460
+ /**
461
+ * Update pitch and bearing together, preserving the current map center.
462
+ * Used by the Ctrl+drag handler to avoid applying two separate transforms.
463
+ *
464
+ * @param {number} pitch Pitch angle in degrees.
465
+ * @param {number} bearing Bearing angle in degrees.
466
+ *
467
+ * @fires L.Map#pitch
468
+ * @fires L.Map#rotate
469
+ *
470
+ * @since leaflet-rotate (0.5.0)
471
+ */
472
+ _setPitchAndBearing(pitch, bearing) {
473
+ if (!this._rotate) { return; }
474
+
475
+ const clampedPitch = Math.max(0, Math.min(pitch, 85)) * (Math.PI / 180),
476
+ clampedBearing = L.Util.wrapNum(bearing, [0, 360]) * (Math.PI / 180),
477
+ pitchChanged = clampedPitch !== this._pitch;
478
+ if (!pitchChanged && clampedBearing === this._bearing) { return; }
479
+
480
+ const mapCenter = this._loaded ? this.getCenter() : undefined;
481
+ if (pitchChanged) {
482
+ this._pitch = clampedPitch;
483
+ this._rotatePane.style.transformOrigin = L.version.startsWith('2.') ? '0 0' : '';
484
+ }
485
+ this._setBearing(bearing, mapCenter);
486
+ if (pitchChanged) {
487
+ this.fire('pitch');
488
+ }
489
+ },
490
+
491
+ /**
492
+ * Get the current map pitch in degrees.
493
+ *
494
+ * @returns {number} Pitch angle in degrees.
495
+ *
496
+ * @since leaflet-rotate (0.5.0)
497
+ */
498
+ getPitch() {
499
+ return (this._pitch || 0) * (180 / Math.PI);
500
+ },
501
+
502
+ /**
503
+ * Get the current map bearing. Enable the `rotate` map option before calling.
504
+ *
505
+ * @returns {number} Bearing in degrees, normalized to [0, 360).
506
+ *
507
+ * @since leaflet-rotate (v0.1.0)
508
+ */
509
+ getBearing() {
510
+ return this._bearing * (180 / Math.PI);
286
511
  },
287
512
 
288
513
  /**
@@ -292,9 +517,12 @@ L.Map.include({
292
517
  *
293
518
  * @param {String} name leaflet pane
294
519
  * @param {HTMLElement} [container] parent element
520
+ *
295
521
  * @returns {HTMLElement} pane container
522
+ *
523
+ * @since leaflet-rotate (v0.1.0)
296
524
  */
297
- // createPane: function(name, container) {
525
+ // createPane(name, container) {
298
526
  // if (!this._rotate || name == 'mapPane') {
299
527
  // return mapProto.createPane.apply(this, arguments);
300
528
  // }
@@ -323,8 +551,14 @@ L.Map.include({
323
551
  * - tooltipPane [HTMLElement = 650] - Pane for tooltips.
324
552
  * - popupPane [HTMLElement = 700] - Pane for popups.
325
553
  */
326
- _initPanes: function() {
327
- var panes = this._panes = {};
554
+ /**
555
+ * Create the map's panes, placing tile/vector layers in the transformed pane
556
+ * and markers/overlays in the pane that remains upright.
557
+ *
558
+ * @since leaflet-rotate (v0.1.0)
559
+ */
560
+ _initPanes() {
561
+ const panes = this._panes = {};
328
562
  this._paneRenderers = {};
329
563
 
330
564
  this._mapPane = this.createPane('mapPane', this._container);
@@ -333,6 +567,9 @@ L.Map.include({
333
567
  if (this._rotate) {
334
568
  this._rotatePane = this.createPane('rotatePane', this._mapPane);
335
569
  this._norotatePane = this.createPane('norotatePane', this._mapPane);
570
+ if (L.version.startsWith('2.')) {
571
+ this._rotatePane.style.transformOrigin = '0 0';
572
+ }
336
573
  // rotatePane
337
574
  this.createPane('tilePane', this._rotatePane);
338
575
  this.createPane('overlayPane', this._rotatePane);
@@ -351,8 +588,8 @@ L.Map.include({
351
588
  }
352
589
 
353
590
  if (!this.options.markerZoomAnimation) {
354
- L.DomUtil.addClass(panes.markerPane, 'leaflet-zoom-hide');
355
- L.DomUtil.addClass(panes.shadowPane, 'leaflet-zoom-hide');
591
+ panes.markerPane.classList.add('leaflet-zoom-hide');
592
+ panes.shadowPane.classList.add('leaflet-zoom-hide');
356
593
  }
357
594
  },
358
595
 
@@ -364,29 +601,31 @@ L.Map.include({
364
601
  *
365
602
  * @see https://github.com/Raruto/leaflet-rotate/issues/18
366
603
  *
367
- * @param {L.LatLng} latlng coordinates
368
- * @param {Object} [options={}] padding options
604
+ * @param {L.LatLng} latlng Geographic point to keep visible.
605
+ * @param {Object} [options={}] Padding options (`padding`, `paddingTopLeft`, `paddingBottomRight`).
369
606
  *
370
607
  * @returns {L.Map} current map instance
608
+ *
609
+ * @since leaflet-rotate (v0.1.4)
371
610
  */
372
611
  panInside(latlng, options) {
373
- if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
612
+ if (!this._rotate || (!this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1)) {
374
613
  return mapProto.panInside.apply(this, arguments);
375
614
  }
376
615
 
377
616
  options = options || {};
378
617
 
379
- const paddingTL = L.point(options.paddingTopLeft || options.padding || [0, 0]),
380
- paddingBR = L.point(options.paddingBottomRight || options.padding || [0, 0]),
618
+ const paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
619
+ paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
381
620
  /** @TODO use mapProto.panInside */
382
621
  // pixelPoint = this.project(latlng),
383
622
  // pixelBounds = this.getPixelBounds(),
384
623
  // pixelCenter = this.project(this.getCenter()),
385
624
  pixelPoint = this.latLngToContainerPoint(latlng),
386
- pixelBounds = L.bounds([L.point(0, 0), this.getSize()]),
625
+ pixelBounds = new L.Bounds([new L.Point(0, 0), this.getSize()]),
387
626
  pixelCenter = pixelBounds.getCenter(),
388
627
  //
389
- paddedBounds = L.bounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
628
+ paddedBounds = new L.Bounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
390
629
  paddedSize = paddedBounds.getSize();
391
630
 
392
631
  if (!paddedBounds.contains(pixelPoint)) {
@@ -414,13 +653,16 @@ L.Map.include({
414
653
  *
415
654
  * @see https://github.com/fnicollet/Leaflet/issues/7
416
655
  *
417
- * @param {L.LatLngBounds} bounds coordinates
418
- * @param {Object} [options] pan options
419
- * @returns {L.Map} current map instance
656
+ * @param {L.LatLngBounds} bounds Geographic bounds to fit inside the viewport.
657
+ * @param {Object} [options] Pan animation options.
658
+ *
659
+ * @returns {L.Map} This map instance.
660
+ *
661
+ * @since leaflet-rotate (v0.1.0)
420
662
  */
421
- // panInsideBounds: function (bounds, options) {
663
+ // panInsideBounds(bounds, options) {
422
664
  // this._enforcingBounds = true;
423
- // var center = this.getCenter(),
665
+ // const center = this.getCenter(),
424
666
  // newCenter = this._limitCenter(center, this._zoom, L.latLngBounds(bounds));
425
667
  //
426
668
  // if (!center.equals(newCenter)) {
@@ -485,19 +727,21 @@ L.Map.include({
485
727
  * instead returns the minimum zoom level on which the map view fits into
486
728
  * the given bounds in its entirety.
487
729
  *
488
- * @param {L.LatLngBounds} bounds
489
- * @param {Boolean} [inside=false]
490
- * @param {L.Point} [padding=[0,0]]
730
+ * @param {L.LatLngBounds} bounds Geographic bounds to fit.
731
+ * @param {boolean} [inside=false] Fit the viewport inside bounds instead of bounds inside the viewport.
732
+ * @param {L.Point|number[]} [padding=[0,0]] Pixel padding around the viewport.
733
+ *
734
+ * @returns {number} Zoom level satisfying the fit.
491
735
  *
492
- * @returns {Number} zoom level
736
+ * @since leaflet-rotate (v0.1.0)
493
737
  */
494
738
  getBoundsZoom(bounds, inside, padding) {
495
- if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
739
+ if (!this._rotate || (!this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1)) {
496
740
  return mapProto.getBoundsZoom.apply(this, arguments);
497
741
  }
498
742
 
499
- bounds = L.latLngBounds(bounds);
500
- padding = L.point(padding || [0, 0]);
743
+ bounds = bounds instanceof L.LatLngBounds ? bounds : new L.LatLngBounds(bounds);
744
+ padding = toPoint(padding || [0, 0]);
501
745
 
502
746
  let zoom = this.getZoom() || 0;
503
747
  const min = this.getMinZoom(),
@@ -529,27 +773,39 @@ L.Map.include({
529
773
  *
530
774
  * @returns {L.Point} layer center
531
775
  */
532
- // _getCenterLayerPoint: function () {
776
+ // _getCenterLayerPoint() {
533
777
  // return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
534
778
  // },
535
779
 
536
780
  /**
537
- * Offset of the specified place to the current center in pixels
781
+ * Return the pixel offset from the current center to the given geographic point.
538
782
  *
539
- * @param {L.LatLng} latlng map coordinates
783
+ * @param {L.LatLng} latlng Geographic point.
784
+ *
785
+ * @returns {L.Point} Offset in layer pixels, adjusted for bearing and pitch.
786
+ *
787
+ * @since leaflet-rotate (v0.1.0)
540
788
  */
541
- _getCenterOffset: function(latlng) {
542
- var centerOffset = mapProto._getCenterOffset.apply(this, arguments);
543
- if (this._rotate) {
789
+ _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) {
544
796
  centerOffset = centerOffset.rotate(this._bearing);
545
797
  }
546
798
  return centerOffset;
547
799
  },
548
800
 
549
801
  /**
550
- * @since leaflet-rotate (v0.1)
802
+ * Return the current position of the rotated pane.
803
+ *
804
+ * @returns {L.Point} Rotated-pane position in map-pane coordinates.
805
+ *
806
+ * @since leaflet-rotate (v0.1.0)
551
807
  */
552
- _getRotatePanePos: function() {
808
+ _getRotatePanePos() {
553
809
  return this._rotatePanePos || new L.Point(0, 0);
554
810
  // return L.DomUtil.getPosition(this._rotatePane) || new L.Point(0, 0);
555
811
  },
@@ -559,11 +815,21 @@ L.Map.include({
559
815
  // return this.project(latlng, zoom)._subtract(topLeft);
560
816
  //},
561
817
 
562
- _getNewPixelOrigin: function(center, zoom) {
563
- if (!this._rotate) {
818
+ /**
819
+ * Calculate the pixel origin for a target view while preserving bearing-aware alignment.
820
+ *
821
+ * @param {L.LatLng} center Target center.
822
+ * @param {number} zoom Target zoom level.
823
+ *
824
+ * @returns {L.Point} New pixel origin.
825
+ *
826
+ * @since leaflet-rotate (v0.1.0)
827
+ */
828
+ _getNewPixelOrigin(center, zoom) {
829
+ if (!this._rotate || L.version.startsWith('2.')) {
564
830
  return mapProto._getNewPixelOrigin.apply(this, arguments);
565
831
  }
566
- var viewHalf = this.getSize()._divideBy(2);
832
+ const viewHalf = this.getSize()._divideBy(2);
567
833
  return this.project(center, zoom)
568
834
  .rotate(this._bearing)
569
835
  ._subtract(viewHalf)
@@ -574,17 +840,24 @@ L.Map.include({
574
840
  },
575
841
 
576
842
  /**
577
- * @since leaflet-rotate (v0.2)
843
+ * Calculate the pixel bounds needed to cover the transformed viewport.
844
+ *
845
+ * @param {L.LatLng} [center=this.getCenter()] Target center.
846
+ * @param {number} [zoom=this.getZoom()] Target zoom level.
847
+ *
848
+ * @returns {L.Bounds} Pixel bounds in projected map coordinates.
849
+ *
850
+ * @since leaflet-rotate (v0.2.0)
578
851
  *
579
852
  * @see src\layer\tile\GridLayer::_getTiledPixelBounds()
580
853
  */
581
- _getNewPixelBounds: function(center, zoom) {
854
+ _getNewPixelBounds(center, zoom) {
582
855
  center = center || this.getCenter();
583
856
  zoom = zoom || this.getZoom();
584
857
  if (!this._rotate && mapProto._getNewPixelBounds) {
585
858
  return mapProto._getNewPixelBounds.apply(this, arguments);
586
859
  }
587
- var mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(),
860
+ const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(),
588
861
  scale = this.getZoomScale(mapZoom, zoom),
589
862
  pixelCenter = this.project(center, zoom).floor(),
590
863
  size = this.getSize(),
@@ -599,11 +872,11 @@ L.Map.include({
599
872
  },
600
873
 
601
874
  /**
602
- * @since leaflet-rotate (v0.2)
875
+ * @since leaflet-rotate (v0.2.0)
603
876
  *
604
- * @return {L.Point} map pivot point (center)
877
+ * @returns {L.Point} Map pivot point in pixel coordinates.
605
878
  */
606
- _getPixelCenter: function() {
879
+ _getPixelCenter() {
607
880
  if (!this._rotate && mapProto._getPixelCenter) {
608
881
  return mapProto._getPixelCenter.apply(this, arguments);
609
882
  }
@@ -611,15 +884,19 @@ L.Map.include({
611
884
  },
612
885
 
613
886
  /**
614
- * @since leaflet-rotate (v0.2)
615
- *
616
887
  * @see src\layer\vector\Renderer::_update()
888
+ *
889
+ * @param {number} padding Bounds padding as a fraction of map size.
890
+ *
891
+ * @returns {L.Bounds} Padded bounds in layer-point coordinates.
892
+ *
893
+ * @since leaflet-rotate (v0.2.0)
617
894
  */
618
- _getPaddedPixelBounds: function(padding) {
895
+ _getPaddedPixelBounds(padding) {
619
896
  if (!this._rotate && mapProto._getPaddedPixelBounds) {
620
897
  return mapProto._getPaddedPixelBounds.apply(this, arguments);
621
898
  }
622
- var p = padding,
899
+ const p = padding,
623
900
  size = this.getSize(),
624
901
  padMin = size.multiplyBy(-p),
625
902
  padMax = size.multiplyBy(1 + p);
@@ -633,10 +910,19 @@ L.Map.include({
633
910
  ]);
634
911
  },
635
912
 
636
- _handleGeolocationResponse: function(pos) {
913
+ /**
914
+ * Build and fire Leaflet's location-found event, including the device heading.
915
+ *
916
+ * @param {GeolocationPosition} pos Browser geolocation result.
917
+ *
918
+ * @fires L.Map#locationfound
919
+ *
920
+ * @since leaflet-rotate (v0.1.0)
921
+ */
922
+ _handleGeolocationResponse(pos) {
637
923
  if (!this._container._leaflet_id) { return; }
638
924
 
639
- var lat = pos.coords.latitude,
925
+ const lat = pos.coords.latitude,
640
926
  lng = pos.coords.longitude,
641
927
  /** @TODO use mapProto._handleGeolocationResponse */
642
928
  hdg = pos.coords.heading,
@@ -645,11 +931,11 @@ L.Map.include({
645
931
  options = this._locateOptions;
646
932
 
647
933
  if (options.setView) {
648
- var zoom = this.getBoundsZoom(bounds);
934
+ const zoom = this.getBoundsZoom(bounds);
649
935
  this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
650
936
  }
651
937
 
652
- var data = {
938
+ const data = {
653
939
  latlng: latlng,
654
940
  bounds: bounds,
655
941
  timestamp: pos.timestamp,
@@ -657,7 +943,7 @@ L.Map.include({
657
943
  heading: hdg
658
944
  };
659
945
 
660
- for (var i in pos.coords) {
946
+ for (const i in pos.coords) {
661
947
  if (typeof pos.coords[i] === 'number') {
662
948
  data[i] = pos.coords[i];
663
949
  }
@@ -673,10 +959,11 @@ L.Map.include({
673
959
  * @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/geo/LatLngBounds.js#L253-L264
674
960
  *
675
961
  * @param {L.Bounds} points
962
+ *
676
963
  * @returns {L.Bounds}
677
964
  */
678
965
  // toCircumscribedBounds(points) {
679
- // var minX = points.reduce(function (pv, v) { return Math.min(pv, v.x); }, points[0].x),
966
+ // const minX = points.reduce(function (pv, v) { return Math.min(pv, v.x); }, points[0].x),
680
967
  // maxX = points.reduce(function (pv, v) { return Math.max(pv, v.x); }, points[0].x),
681
968
  // minY = points.reduce(function (pv, v) { return Math.min(pv, v.y); }, points[0].y),
682
969
  // maxY = points.reduce(function (pv, v) { return Math.max(pv, v.y); }, points[0].y);