leaflet-rotate 0.4.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.
- package/README.md +85 -5
- package/dist/leaflet-rotate-src.js +1325 -254
- package/dist/leaflet-rotate-src.js.map +4 -4
- package/dist/leaflet-rotate.js +1 -1
- package/dist/leaflet-rotate.js.map +4 -4
- package/lib/debug.js +62 -88
- package/lib/debug.min.js +16 -1
- package/misc/leaflet-heat-rotate.js +7 -0
- package/package.json +1 -1
- package/src/control/Rotate.js +160 -103
- package/src/dom/DomUtil.js +23 -12
- package/src/dom/Draggable.js +5 -0
- package/src/geo/LatLngBounds.js +5 -0
- package/src/geometry/Bounds.js +5 -0
- package/src/geometry/Point.js +17 -3
- package/src/index.js +4 -1
- package/src/layer/DivOverlay.js +34 -5
- package/src/layer/Popup.js +14 -3
- package/src/layer/Tooltip.js +16 -0
- package/src/layer/marker/Icon.js +12 -0
- package/src/layer/marker/Marker.js +86 -7
- package/src/layer/tile/GridLayer.js +38 -3
- package/src/layer/vector/Renderer.js +88 -34
- package/src/map/Map.js +578 -116
- package/src/map/handler/CompassBearing.js +34 -1
- package/src/map/handler/ContainerMutation.js +18 -1
- package/src/map/handler/ShiftKeyRotate.js +204 -13
- package/src/map/handler/TouchGestures.js +108 -10
- package/src/map/handler/TouchPitch.js +53 -0
- package/src/map/handler/TouchRotate.js +18 -1
- package/src/map/handler/TouchZoom.js +21 -1
- package/test/features/example.feature +8 -4
- package/test/features/gestures.feature +4 -0
- package/test/features/rotation.feature +60 -0
- package/test/index.html +342 -186
- package/test/steps/bearing.steps.js +535 -0
- package/test/steps/example.steps.js +56 -29
- package/test/steps/gestures.steps.js +55 -0
- package/test/support/hooks.js +4 -16
|
@@ -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 {
|
|
36
|
-
* @param {
|
|
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 {
|
|
54
|
-
* @param {L.Point} pivot
|
|
55
|
-
* @param {
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
145
|
-
*
|
|
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
|
-
*
|
|
174
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
430
|
+
const events = Object.assign(markerProto.getEvents.apply(this, arguments), {
|
|
431
|
+
rotate: this.update,
|
|
432
|
+
pitch: this._updatePitchEvents
|
|
433
|
+
});
|
|
434
|
+
if (this._map && this._map._pitch) {
|
|
435
|
+
events.move = this.update;
|
|
436
|
+
}
|
|
437
|
+
return events;
|
|
438
|
+
},
|
|
439
|
+
/**
|
|
440
|
+
* Subscribe to map movement while pitch is active to keep the icon aligned.
|
|
441
|
+
*
|
|
442
|
+
* @since leaflet-rotate (0.5.0)
|
|
443
|
+
*/
|
|
444
|
+
_updatePitchEvents() {
|
|
445
|
+
if (this._map._pitch) {
|
|
446
|
+
this._map.on("move", this.update, this);
|
|
447
|
+
} else {
|
|
448
|
+
this._map.off("move", this.update, this);
|
|
449
|
+
}
|
|
335
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
|
-
*
|
|
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);
|
|
@@ -398,10 +537,39 @@ var leafletRotate = (() => {
|
|
|
398
537
|
if (!this._onRotate) {
|
|
399
538
|
this._onRotate = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
|
|
400
539
|
}
|
|
401
|
-
events.rotate = this.
|
|
540
|
+
events.rotate = this._onRotateEvent;
|
|
402
541
|
}
|
|
403
542
|
return events;
|
|
404
543
|
},
|
|
544
|
+
/**
|
|
545
|
+
* Keep tile levels aligned when a bearing/pitch change shifts the pixel origin
|
|
546
|
+
* (eg. when the rotation pivot is not the view center), then refresh tiles (throttled).
|
|
547
|
+
*
|
|
548
|
+
* @listens L.Map~rotate
|
|
549
|
+
*
|
|
550
|
+
* @since leaflet-rotate (0.5.1)
|
|
551
|
+
*/
|
|
552
|
+
_onRotateEvent() {
|
|
553
|
+
const map = this._map;
|
|
554
|
+
if (!map) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const origin = map.getPixelOrigin();
|
|
558
|
+
if (this._rotateOrigin && !this._rotateOrigin.equals(origin) && !map._animatingZoom && this._levels) {
|
|
559
|
+
this._setZoomTransforms(map.getCenter(), map.getZoom());
|
|
560
|
+
}
|
|
561
|
+
this._rotateOrigin = origin;
|
|
562
|
+
this._onRotate();
|
|
563
|
+
},
|
|
564
|
+
/**
|
|
565
|
+
* Return tile pixel bounds that cover the rotated viewport.
|
|
566
|
+
*
|
|
567
|
+
* @param {L.LatLng} center View center used to calculate bounds.
|
|
568
|
+
*
|
|
569
|
+
* @returns {L.Bounds} Pixel bounds covered by the layer's tiles.
|
|
570
|
+
*
|
|
571
|
+
* @since leaflet-rotate (v0.1.0)
|
|
572
|
+
*/
|
|
405
573
|
_getTiledPixelBounds(center) {
|
|
406
574
|
if (!this._map._rotate) {
|
|
407
575
|
return gridLayerProto._getTiledPixelBounds.apply(this, arguments);
|
|
@@ -414,21 +582,73 @@ var leafletRotate = (() => {
|
|
|
414
582
|
var rendererProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Renderer.prototype));
|
|
415
583
|
var rendererBase = Object.getPrototypeOf(L.Renderer.prototype);
|
|
416
584
|
var rendererGetEvents = rendererProto.getEvents || rendererBase.getEvents;
|
|
585
|
+
var rendererOnMoveEnd = rendererProto._onMoveEnd || rendererBase._onMoveEnd;
|
|
417
586
|
L.Renderer.include({
|
|
418
587
|
/**
|
|
419
|
-
*
|
|
420
|
-
* map is moved by just calling `map.setBearing(theta)`
|
|
588
|
+
* Subscribe the renderer to map rotation so its visible bounds are recalculated.
|
|
421
589
|
*
|
|
422
590
|
* @listens L.Map~rotate
|
|
591
|
+
*
|
|
592
|
+
* @returns {Object} Leaflet event map for the renderer.
|
|
593
|
+
*
|
|
594
|
+
* @since leaflet-rotate (v0.1.0)
|
|
423
595
|
*/
|
|
424
596
|
getEvents() {
|
|
425
597
|
const events = rendererGetEvents.apply(this, arguments);
|
|
426
|
-
return
|
|
598
|
+
return Object.assign(events, { rotate: this._onRotate });
|
|
599
|
+
},
|
|
600
|
+
/**
|
|
601
|
+
* Refresh renderer bounds after bearing or pitch changes.
|
|
602
|
+
*
|
|
603
|
+
* Paths are reprojected only when the pixel origin actually changed (eg. fractional centers
|
|
604
|
+
* after an off-center zoom), since reprojection is the main cost with long polylines.
|
|
605
|
+
*
|
|
606
|
+
* @since leaflet-rotate (v0.1.0)
|
|
607
|
+
*/
|
|
608
|
+
_onRotate() {
|
|
609
|
+
if (!this._map._rotate) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const origin = this._map.getPixelOrigin();
|
|
613
|
+
if (!this._rotateOrigin || !this._rotateOrigin.equals(origin)) {
|
|
614
|
+
this._rotateOrigin = origin;
|
|
615
|
+
clearTimeout(this._rotateTimer);
|
|
616
|
+
this._onZoomEnd();
|
|
617
|
+
this._updateAfterRotate();
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (this._bounds && this._bounds.contains(this._map._getPaddedPixelBounds(0))) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
clearTimeout(this._rotateTimer);
|
|
624
|
+
const wait = (this._lastRotateUpdate || 0) + 100 - performance.now();
|
|
625
|
+
if (wait > 0) {
|
|
626
|
+
this._rotateTimer = setTimeout(() => this._map && this._updateAfterRotate(), wait);
|
|
627
|
+
} else {
|
|
628
|
+
this._updateAfterRotate();
|
|
629
|
+
}
|
|
630
|
+
},
|
|
631
|
+
/**
|
|
632
|
+
* Recalculate renderer bounds and redraw after bearing or pitch changes.
|
|
633
|
+
*
|
|
634
|
+
* @since leaflet-rotate (0.5.1)
|
|
635
|
+
*/
|
|
636
|
+
_updateAfterRotate() {
|
|
637
|
+
this._lastRotateUpdate = performance.now();
|
|
638
|
+
if (L.version.startsWith("2.")) {
|
|
639
|
+
this._onMoveEnd();
|
|
640
|
+
} else {
|
|
641
|
+
this._update();
|
|
642
|
+
}
|
|
427
643
|
},
|
|
428
644
|
/**
|
|
429
|
-
*
|
|
645
|
+
* Add the zoom-animation class needed for correct renderer transforms in Leaflet 1.
|
|
646
|
+
*
|
|
647
|
+
* Fixes `map.flyTo()` when `map.options.zoomAnimation` is false.
|
|
430
648
|
*
|
|
431
649
|
* @see https://github.com/Leaflet/Leaflet/pull/8794
|
|
650
|
+
*
|
|
651
|
+
* @since leaflet-rotate (v0.2.0)
|
|
432
652
|
*/
|
|
433
653
|
onAdd() {
|
|
434
654
|
rendererProto.onAdd.apply(this, arguments);
|
|
@@ -437,45 +657,38 @@ var leafletRotate = (() => {
|
|
|
437
657
|
}
|
|
438
658
|
},
|
|
439
659
|
/**
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* the main cause seems to be related to `this._updateTransform(path._center, path._zoom))`
|
|
443
|
-
* and `this._topLeft = this._map.layerPointToLatLng(this._bounds.min);`
|
|
444
|
-
*
|
|
445
|
-
* @example
|
|
446
|
-
* map.setZoom(2);
|
|
447
|
-
* path._renderer._update();
|
|
448
|
-
* path._renderer._updateTransform(path._renderer._center, path._renderer._zoom);
|
|
660
|
+
* Update the renderer transform during zoom animation, accounting for rotated coordinates.
|
|
449
661
|
*
|
|
450
662
|
* @see https://github.com/Leaflet/Leaflet/pull/8794
|
|
451
663
|
* @see https://github.com/Leaflet/Leaflet/pull/8103
|
|
452
664
|
* @see https://github.com/Leaflet/Leaflet/issues/7466
|
|
453
665
|
*
|
|
454
|
-
* @
|
|
666
|
+
* @param {L.LatLng} center Target map center.
|
|
667
|
+
* @param {number} zoom Target zoom level.
|
|
455
668
|
*
|
|
456
|
-
* @
|
|
669
|
+
* @since leaflet-rotate (v0.1.0)
|
|
457
670
|
*/
|
|
458
671
|
_updateTransform(center, zoom) {
|
|
459
|
-
|
|
460
|
-
if (this._map._rotate) {
|
|
461
|
-
const scale2 = this._map.getZoomScale(zoom, this._zoom), offset2 = this._topLeftPoint.multiplyBy(scale2).subtract(this._map._getNewPixelOrigin(center, zoom));
|
|
462
|
-
L.DomUtil.setTransform(this._container, offset2, scale2);
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
return rendererBase._updateTransform.apply(this, arguments);
|
|
466
|
-
}
|
|
672
|
+
const v2 = L.version.startsWith("2.");
|
|
467
673
|
if (!this._map._rotate) {
|
|
468
|
-
return rendererProto._updateTransform.apply(this, arguments);
|
|
674
|
+
return (v2 ? rendererBase : rendererProto)._updateTransform.apply(this, arguments);
|
|
469
675
|
}
|
|
470
|
-
if (zoom === this._zoom) {
|
|
676
|
+
if (!v2 && zoom === this._zoom) {
|
|
471
677
|
return;
|
|
472
678
|
}
|
|
473
|
-
const scale = this._map.getZoomScale(zoom, this._zoom), offset = this.
|
|
679
|
+
const scale = this._map.getZoomScale(zoom, this._zoom), offset = this._topLeftPoint.multiplyBy(scale).subtract(this._map._getNewPixelOrigin(center, zoom));
|
|
474
680
|
L.DomUtil.setTransform(this._container, offset, scale);
|
|
475
681
|
},
|
|
682
|
+
/**
|
|
683
|
+
* Recalculate bounds and finish renderer updates after movement on Leaflet 2.
|
|
684
|
+
*
|
|
685
|
+
* @param {Object} ev Map movement event.
|
|
686
|
+
*
|
|
687
|
+
* @since leaflet-rotate (v0.4.0)
|
|
688
|
+
*/
|
|
476
689
|
_onMoveEnd(ev) {
|
|
477
690
|
if (!L.version.startsWith("2.") || !this._map._rotate) {
|
|
478
|
-
return
|
|
691
|
+
return rendererOnMoveEnd.apply(this, arguments);
|
|
479
692
|
}
|
|
480
693
|
this._bounds = this._map._getPaddedPixelBounds(this.options.padding);
|
|
481
694
|
this._topLeft = this._map.layerPointToLatLng(this._bounds.min);
|
|
@@ -486,6 +699,11 @@ var leafletRotate = (() => {
|
|
|
486
699
|
this._resizeContainer();
|
|
487
700
|
this._onSettled(ev);
|
|
488
701
|
},
|
|
702
|
+
/**
|
|
703
|
+
* Resize the renderer container to cover the pitch-aware map bounds.
|
|
704
|
+
*
|
|
705
|
+
* @since leaflet-rotate (v0.4.0)
|
|
706
|
+
*/
|
|
489
707
|
_resizeContainer() {
|
|
490
708
|
if (!L.version.startsWith("2.") || !this._map._rotate || !this._bounds) {
|
|
491
709
|
return rendererBase._resizeContainer.apply(this, arguments);
|
|
@@ -530,6 +748,11 @@ var leafletRotate = (() => {
|
|
|
530
748
|
// this._layers[id]._update();
|
|
531
749
|
// }
|
|
532
750
|
// },
|
|
751
|
+
/**
|
|
752
|
+
* Recalculate renderer bounds and cached view state.
|
|
753
|
+
*
|
|
754
|
+
* @since leaflet-rotate (v0.1.0)
|
|
755
|
+
*/
|
|
533
756
|
_update() {
|
|
534
757
|
if (!this._map._rotate) {
|
|
535
758
|
return rendererProto._update.apply(this, arguments);
|
|
@@ -538,13 +761,49 @@ var leafletRotate = (() => {
|
|
|
538
761
|
this._topLeft = this._map.layerPointToLatLng(this._bounds.min);
|
|
539
762
|
this._center = this._map.getCenter();
|
|
540
763
|
this._zoom = this._map.getZoom();
|
|
764
|
+
this._topLeftPoint = this._map._getNewPixelOrigin(this._center, this._zoom).add(this._bounds.min);
|
|
541
765
|
}
|
|
542
766
|
});
|
|
543
767
|
|
|
544
768
|
// src/map/Map.js
|
|
545
769
|
var mapProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Map.prototype));
|
|
546
|
-
L.Map.mergeOptions({
|
|
770
|
+
L.Map.mergeOptions({
|
|
771
|
+
/**
|
|
772
|
+
* @type {boolean} Enable bearing and pitch transformations.
|
|
773
|
+
*
|
|
774
|
+
* @since leaflet-rotate (v0.1.0)
|
|
775
|
+
*/
|
|
776
|
+
rotate: false,
|
|
777
|
+
/**
|
|
778
|
+
* @type {number} Initial map bearing in degrees.
|
|
779
|
+
*
|
|
780
|
+
* @since leaflet-rotate (v0.1.0)
|
|
781
|
+
*/
|
|
782
|
+
bearing: 0,
|
|
783
|
+
/**
|
|
784
|
+
* @type {number} Initial map pitch in degrees, clamped to 0–85.
|
|
785
|
+
*
|
|
786
|
+
* @since leaflet-rotate (0.5.0)
|
|
787
|
+
*/
|
|
788
|
+
pitch: 0,
|
|
789
|
+
/**
|
|
790
|
+
* @type {number} Maximum distance of the pitched map plane from the map center, in multiples
|
|
791
|
+
* of the map height. Tiles, vectors and `getBounds()` stop there (and always
|
|
792
|
+
* before the horizon); a `.leaflet-pitch-sky` element fills the area above.
|
|
793
|
+
*
|
|
794
|
+
* @since leaflet-rotate (0.5.1)
|
|
795
|
+
*/
|
|
796
|
+
pitchMaxDistance: 3
|
|
797
|
+
});
|
|
547
798
|
L.Map.include({
|
|
799
|
+
/**
|
|
800
|
+
* Apply an immediate animated-zoom frame without waiting for a transition delay.
|
|
801
|
+
*
|
|
802
|
+
* @param {L.LatLng} center Target center.
|
|
803
|
+
* @param {number} zoom Target zoom level.
|
|
804
|
+
*
|
|
805
|
+
* @since leaflet-rotate (v0.1.0)
|
|
806
|
+
*/
|
|
548
807
|
_animateZoomNoDelay(center, zoom) {
|
|
549
808
|
if (!this._mapPane) {
|
|
550
809
|
return;
|
|
@@ -556,17 +815,25 @@ var leafletRotate = (() => {
|
|
|
556
815
|
this._onZoomTransitionEnd();
|
|
557
816
|
},
|
|
558
817
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
818
|
+
* Initialize the Leaflet map and apply its initial bearing and pitch.
|
|
819
|
+
*
|
|
820
|
+
* @param {HTMLElement|string} id Map container element or its ID.
|
|
821
|
+
* @param {Object} [options={}] Leaflet map options, including `rotate`, `bearing`, and `pitch`.
|
|
822
|
+
*
|
|
823
|
+
* @since leaflet-rotate (v0.1.0)
|
|
561
824
|
*/
|
|
562
825
|
initialize(id, options) {
|
|
563
826
|
if (options.rotate) {
|
|
564
827
|
this._rotate = true;
|
|
565
828
|
this._bearing = 0;
|
|
829
|
+
this._pitch = 0;
|
|
566
830
|
}
|
|
567
831
|
mapProto.initialize.apply(this, arguments);
|
|
568
832
|
if (this.options.rotate) {
|
|
833
|
+
this.on("resize", this._updatePitchTransform, this);
|
|
834
|
+
this.on("move", this._updatePitchSky, this);
|
|
569
835
|
this.setBearing(this.options.bearing);
|
|
836
|
+
this.setPitch(this.options.pitch);
|
|
570
837
|
}
|
|
571
838
|
},
|
|
572
839
|
/**
|
|
@@ -574,26 +841,41 @@ var leafletRotate = (() => {
|
|
|
574
841
|
* returns the corresponding pixel coordinate relative to
|
|
575
842
|
* the [origin pixel](#map-getpixelorigin).
|
|
576
843
|
*
|
|
577
|
-
* @param {L.Point} point
|
|
578
|
-
*
|
|
844
|
+
* @param {L.Point|number[]} point Pixel coordinate in the map container.
|
|
845
|
+
*
|
|
846
|
+
* @returns {L.Point} Corresponding layer-point coordinate.
|
|
847
|
+
*
|
|
848
|
+
* @since leaflet-rotate (v0.1.0)
|
|
579
849
|
*/
|
|
580
850
|
containerPointToLayerPoint(point) {
|
|
581
851
|
if (!this._rotate) {
|
|
582
852
|
return mapProto.containerPointToLayerPoint.apply(this, arguments);
|
|
583
853
|
}
|
|
854
|
+
if (this._pitch) {
|
|
855
|
+
return this._transformPitchPoint(
|
|
856
|
+
toPoint(point).subtract(this._getMapPanePos()),
|
|
857
|
+
this._pitchInverseTransform
|
|
858
|
+
);
|
|
859
|
+
}
|
|
584
860
|
return toPoint(point).subtract(this._getMapPanePos()).rotateFrom(-this._bearing, this._getRotatePanePos()).subtract(this._getRotatePanePos());
|
|
585
861
|
},
|
|
586
862
|
/**
|
|
587
863
|
* Given a pixel coordinate relative to the [origin pixel](#map-getpixelorigin),
|
|
588
864
|
* returns the corresponding pixel coordinate relative to the map container.
|
|
589
865
|
*
|
|
590
|
-
* @param {L.Point} point
|
|
591
|
-
*
|
|
866
|
+
* @param {L.Point|number[]} point Pixel coordinate in layer-point space.
|
|
867
|
+
*
|
|
868
|
+
* @returns {L.Point} Corresponding pixel coordinate in the map container.
|
|
869
|
+
*
|
|
870
|
+
* @since leaflet-rotate (v0.1.0)
|
|
592
871
|
*/
|
|
593
872
|
layerPointToContainerPoint(point) {
|
|
594
873
|
if (!this._rotate) {
|
|
595
874
|
return mapProto.layerPointToContainerPoint.apply(this, arguments);
|
|
596
875
|
}
|
|
876
|
+
if (this._pitch) {
|
|
877
|
+
return this._transformPitchPoint(point, this._pitchTransform).add(this._getMapPanePos());
|
|
878
|
+
}
|
|
597
879
|
return toPoint(point).add(this._getRotatePanePos()).rotateFrom(this._bearing, this._getRotatePanePos()).add(this._getMapPanePos());
|
|
598
880
|
},
|
|
599
881
|
/**
|
|
@@ -603,12 +885,16 @@ var leafletRotate = (() => {
|
|
|
603
885
|
* (rotatePane) --> (mapPane)
|
|
604
886
|
* (rotatePane) --> (norotatePane)
|
|
605
887
|
*
|
|
606
|
-
* @param {L.Point} point
|
|
607
|
-
* @returns {L.Point}
|
|
888
|
+
* @param {L.Point|number[]} point Point in the rotated pane.
|
|
608
889
|
*
|
|
609
|
-
* @
|
|
890
|
+
* @returns {L.Point} Corresponding point in map-pane coordinates.
|
|
891
|
+
*
|
|
892
|
+
* @since leaflet-rotate (v0.1.0)
|
|
610
893
|
*/
|
|
611
894
|
rotatedPointToMapPanePoint(point) {
|
|
895
|
+
if (this._pitch) {
|
|
896
|
+
return this._transformPitchPoint(point, this._pitchTransform);
|
|
897
|
+
}
|
|
612
898
|
return toPoint(point).rotate(this._bearing)._add(this._getRotatePanePos());
|
|
613
899
|
},
|
|
614
900
|
/**
|
|
@@ -618,11 +904,16 @@ var leafletRotate = (() => {
|
|
|
618
904
|
* (mapPane) --> (rotatePane)
|
|
619
905
|
* (norotatePane) --> (rotatePane)
|
|
620
906
|
*
|
|
621
|
-
* @param {L.Point} point
|
|
907
|
+
* @param {L.Point|number[]} point Point in map-pane coordinates.
|
|
622
908
|
*
|
|
623
|
-
* @
|
|
909
|
+
* @returns {L.Point} Corresponding point in the rotated pane.
|
|
910
|
+
*
|
|
911
|
+
* @since leaflet-rotate (v0.1.0)
|
|
624
912
|
*/
|
|
625
913
|
mapPanePointToRotatedPoint(point) {
|
|
914
|
+
if (this._pitch) {
|
|
915
|
+
return this._transformPitchPoint(point, this._pitchInverseTransform);
|
|
916
|
+
}
|
|
626
917
|
return toPoint(point)._subtract(this._getRotatePanePos()).rotate(-this._bearing);
|
|
627
918
|
},
|
|
628
919
|
// latLngToLayerPoint(latlng) {
|
|
@@ -638,10 +929,11 @@ var leafletRotate = (() => {
|
|
|
638
929
|
*
|
|
639
930
|
* @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/map/Map.js#L1114-L1135
|
|
640
931
|
*
|
|
641
|
-
* @param {L.LatLngBounds} bounds
|
|
642
|
-
*
|
|
932
|
+
* @param {L.LatLngBounds} bounds Geographic bounds to project.
|
|
933
|
+
*
|
|
934
|
+
* @returns {L.Bounds} Axis-aligned bounds in container-pixel coordinates.
|
|
643
935
|
*
|
|
644
|
-
* @since leaflet-rotate (v0.2)
|
|
936
|
+
* @since leaflet-rotate (v0.2.0)
|
|
645
937
|
*/
|
|
646
938
|
mapBoundsToContainerBounds(bounds) {
|
|
647
939
|
if (!this._rotate && mapProto.mapBoundsToContainerBounds) {
|
|
@@ -657,31 +949,72 @@ var leafletRotate = (() => {
|
|
|
657
949
|
]);
|
|
658
950
|
},
|
|
659
951
|
/**
|
|
660
|
-
*
|
|
952
|
+
* Return the geographic bounds of the current viewport, accounting for rotation and pitch.
|
|
661
953
|
*
|
|
662
|
-
*
|
|
663
|
-
* function should match the `rotatePane` or `norotatePane` bounds
|
|
954
|
+
* The result is the smallest geographic bounds containing all four viewport corners.
|
|
664
955
|
*
|
|
665
|
-
* @
|
|
956
|
+
* @returns {L.LatLngBounds} Geographic bounds visible in the map viewport.
|
|
666
957
|
*
|
|
667
|
-
* @
|
|
958
|
+
* @since leaflet-rotate (v0.1.0)
|
|
668
959
|
*/
|
|
669
960
|
getBounds() {
|
|
670
961
|
if (!this._rotate) {
|
|
671
962
|
return mapProto.getBounds.apply(this, arguments);
|
|
672
963
|
}
|
|
673
|
-
const
|
|
964
|
+
const [min, max] = this._getPitchedContainerBounds(0);
|
|
674
965
|
return new L.LatLngBounds([
|
|
675
|
-
this.containerPointToLatLng([
|
|
966
|
+
this.containerPointToLatLng([min.x, min.y]),
|
|
676
967
|
// topleft
|
|
677
|
-
this.containerPointToLatLng([
|
|
968
|
+
this.containerPointToLatLng([max.x, min.y]),
|
|
678
969
|
// topright
|
|
679
|
-
this.containerPointToLatLng([
|
|
970
|
+
this.containerPointToLatLng([max.x, max.y]),
|
|
680
971
|
// bottomright
|
|
681
|
-
this.containerPointToLatLng([
|
|
972
|
+
this.containerPointToLatLng([min.x, max.y])
|
|
682
973
|
// bottomleft
|
|
683
974
|
]);
|
|
684
975
|
},
|
|
976
|
+
/**
|
|
977
|
+
* Return the container rectangle covering the visible map plane, optionally padded by a
|
|
978
|
+
* fraction of the map size. When pitched, its top is cut at the far plane (see `pitchMaxDistance`)
|
|
979
|
+
* so that it never reaches the horizon, where the plane would extend to infinity.
|
|
980
|
+
*
|
|
981
|
+
* @param {number} padding Padding as a fraction of the map size.
|
|
982
|
+
*
|
|
983
|
+
* @returns {L.Point[]} Top-left and bottom-right container points.
|
|
984
|
+
*
|
|
985
|
+
* @since leaflet-rotate (0.5.1)
|
|
986
|
+
*/
|
|
987
|
+
_getPitchedContainerBounds(padding) {
|
|
988
|
+
const size = this.getSize(), min = size.multiplyBy(-padding), max = size.multiplyBy(1 + padding);
|
|
989
|
+
if (this._pitch) {
|
|
990
|
+
min.y = Math.min(Math.max(min.y, this._getPitchFarY()), max.y);
|
|
991
|
+
}
|
|
992
|
+
return [min, max];
|
|
993
|
+
},
|
|
994
|
+
/**
|
|
995
|
+
* Return the container y coordinate of the far plane: points above it are farther than
|
|
996
|
+
* `pitchMaxDistance` from the map center, or beyond the horizon.
|
|
997
|
+
*
|
|
998
|
+
* @returns {number} Container y coordinate (`-Infinity` when the whole map is close enough).
|
|
999
|
+
*
|
|
1000
|
+
* @since leaflet-rotate (0.5.1)
|
|
1001
|
+
*/
|
|
1002
|
+
_getPitchFarY() {
|
|
1003
|
+
const size = this.getSize(), inverse = this._pitchInverseTransform, panePos = this._getMapPanePos(), x = size.x / 2 - panePos.x, centerY = size.y / 2 - panePos.y, maxDistance = this.options.pitchMaxDistance * size.y, w = (y) => inverse.g * x + inverse.h * y + inverse.i, centerW = w(centerY), center = this._transformPitchPoint([x, centerY], inverse), isNear = (y) => w(y) * centerW > 0 && this._transformPitchPoint([x, y], inverse).distanceTo(center) <= maxDistance;
|
|
1004
|
+
let far = -size.y - panePos.y, near = centerY;
|
|
1005
|
+
if (isNear(far)) {
|
|
1006
|
+
return -Infinity;
|
|
1007
|
+
}
|
|
1008
|
+
for (let i = 0; i < 20; i++) {
|
|
1009
|
+
const mid = (far + near) / 2;
|
|
1010
|
+
if (isNear(mid)) {
|
|
1011
|
+
near = mid;
|
|
1012
|
+
} else {
|
|
1013
|
+
far = mid;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return near + panePos.y;
|
|
1017
|
+
},
|
|
685
1018
|
/**
|
|
686
1019
|
* Returns the bounds of the current map view in projected pixel
|
|
687
1020
|
* coordinates (sometimes useful in layer and overlay implementations).
|
|
@@ -699,95 +1032,346 @@ var leafletRotate = (() => {
|
|
|
699
1032
|
// return new L.Bounds(topLeftPoint, topLeftPoint.add(this.getSize()));
|
|
700
1033
|
// },
|
|
701
1034
|
/**
|
|
702
|
-
*
|
|
1035
|
+
* Set the map bearing, optionally animating to the new value.
|
|
1036
|
+
*
|
|
1037
|
+
* @param {number} theta Target bearing in degrees; normalized to [0, 360).
|
|
1038
|
+
* @param {Object} [options] Animation options.
|
|
1039
|
+
* @param {boolean} [options.animate=false] Animate the change.
|
|
1040
|
+
* @param {number} [options.duration=0.25] Animation duration in seconds.
|
|
1041
|
+
* @param {number} [options.easeLinearity=0.5] Easing curve linearity.
|
|
703
1042
|
*
|
|
704
|
-
* @
|
|
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]
|
|
1043
|
+
* @returns {void}
|
|
709
1044
|
*
|
|
710
|
-
* @
|
|
1045
|
+
* @fires L.Map#rotate
|
|
1046
|
+
*
|
|
1047
|
+
* @since leaflet-rotate (v0.1.0)
|
|
711
1048
|
*/
|
|
712
1049
|
setBearing(theta, options) {
|
|
713
1050
|
if (!this._rotate) {
|
|
714
1051
|
return;
|
|
715
1052
|
}
|
|
716
1053
|
if (options && options.animate) {
|
|
717
|
-
this.
|
|
718
|
-
const animationId = this._bearingAnimationId;
|
|
719
|
-
if (this._bearingAnimationFrame) {
|
|
720
|
-
cancelAnimationFrame(this._bearingAnimationFrame);
|
|
721
|
-
}
|
|
722
|
-
const startBearing = this.getBearing(), endBearing = L.Util.wrapNum(theta, [0, 360]), delta = (endBearing - startBearing + 540) % 360 - 180, duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1e3, easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2), startTime = performance.now();
|
|
723
|
-
const animate = function() {
|
|
724
|
-
if (!this._mapPane) {
|
|
725
|
-
delete this._bearingAnimationFrame;
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1, eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
|
|
729
|
-
this._setBearing(progress === 1 ? endBearing : startBearing + delta * eased);
|
|
730
|
-
if (this._bearingAnimationId !== animationId) {
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (progress < 1) {
|
|
734
|
-
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
735
|
-
} else {
|
|
736
|
-
delete this._bearingAnimationFrame;
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
1054
|
+
this._animatePitchBearing(this.getPitch(), theta, options);
|
|
740
1055
|
return;
|
|
741
1056
|
}
|
|
1057
|
+
this._stopPitchBearingAnimation();
|
|
1058
|
+
this._setBearing(theta);
|
|
1059
|
+
},
|
|
1060
|
+
/**
|
|
1061
|
+
* Animate pitch and bearing together (bearing follows the shortest path).
|
|
1062
|
+
*
|
|
1063
|
+
* @param {number} pitch Target pitch in degrees.
|
|
1064
|
+
* @param {number} bearing Target bearing in degrees.
|
|
1065
|
+
* @param {Object} [options] Animation options.
|
|
1066
|
+
* @param {number} [options.duration=0.25] Animation duration in seconds.
|
|
1067
|
+
* @param {number} [options.easeLinearity=0.5] Easing curve linearity.
|
|
1068
|
+
*
|
|
1069
|
+
* @since leaflet-rotate (0.5.1)
|
|
1070
|
+
*/
|
|
1071
|
+
_animatePitchBearing(pitch, bearing, options) {
|
|
1072
|
+
this._stopPitchBearingAnimation();
|
|
1073
|
+
const animationId = this._bearingAnimationId;
|
|
1074
|
+
const startBearing = this.getBearing(), startPitch = this.getPitch(), endBearing = L.Util.wrapNum(bearing, [0, 360]), endPitch = Math.max(0, Math.min(pitch, 85)), delta = (endBearing - startBearing + 540) % 360 - 180, duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1e3, easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2), startTime = performance.now();
|
|
1075
|
+
const animate = function() {
|
|
1076
|
+
if (!this._mapPane || this._bearingAnimationId !== animationId) {
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1, eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
|
|
1080
|
+
if (progress < 1) {
|
|
1081
|
+
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
1082
|
+
} else {
|
|
1083
|
+
delete this._bearingAnimationFrame;
|
|
1084
|
+
}
|
|
1085
|
+
this._setPitchAndBearing(
|
|
1086
|
+
progress === 1 ? endPitch : startPitch + (endPitch - startPitch) * eased,
|
|
1087
|
+
progress === 1 ? endBearing : startBearing + delta * eased
|
|
1088
|
+
);
|
|
1089
|
+
};
|
|
1090
|
+
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
1091
|
+
},
|
|
1092
|
+
/**
|
|
1093
|
+
* Cancel a running pitch/bearing animation.
|
|
1094
|
+
*
|
|
1095
|
+
* @since leaflet-rotate (0.5.1)
|
|
1096
|
+
*/
|
|
1097
|
+
_stopPitchBearingAnimation() {
|
|
742
1098
|
this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
|
|
743
1099
|
if (this._bearingAnimationFrame) {
|
|
744
1100
|
cancelAnimationFrame(this._bearingAnimationFrame);
|
|
745
1101
|
delete this._bearingAnimationFrame;
|
|
746
1102
|
}
|
|
747
|
-
this._setBearing(theta);
|
|
748
1103
|
},
|
|
749
|
-
|
|
1104
|
+
/**
|
|
1105
|
+
* Rotate the map back to north (bearing 0), animated by default.
|
|
1106
|
+
*
|
|
1107
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
1108
|
+
*
|
|
1109
|
+
* @returns {L.Map} This map instance.
|
|
1110
|
+
*
|
|
1111
|
+
* @since leaflet-rotate (0.5.1)
|
|
1112
|
+
*/
|
|
1113
|
+
resetNorth(options) {
|
|
1114
|
+
options = Object.assign({ animate: true, duration: 0.5 }, options);
|
|
1115
|
+
if (options.animate) {
|
|
1116
|
+
this._animatePitchBearing(this.getPitch(), 0, options);
|
|
1117
|
+
} else {
|
|
1118
|
+
this.setBearing(0);
|
|
1119
|
+
}
|
|
1120
|
+
return this;
|
|
1121
|
+
},
|
|
1122
|
+
/**
|
|
1123
|
+
* Rotate the map back to north and remove the pitch, animated by default.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
1126
|
+
*
|
|
1127
|
+
* @returns {L.Map} This map instance.
|
|
1128
|
+
*
|
|
1129
|
+
* @since leaflet-rotate (0.5.1)
|
|
1130
|
+
*/
|
|
1131
|
+
resetNorthPitch(options) {
|
|
1132
|
+
options = Object.assign({ animate: true, duration: 0.5 }, options);
|
|
1133
|
+
if (options.animate) {
|
|
1134
|
+
this._animatePitchBearing(0, 0, options);
|
|
1135
|
+
} else {
|
|
1136
|
+
this._stopPitchBearingAnimation();
|
|
1137
|
+
this._setPitchAndBearing(0, 0);
|
|
1138
|
+
}
|
|
1139
|
+
return this;
|
|
1140
|
+
},
|
|
1141
|
+
/**
|
|
1142
|
+
* Apply a bearing immediately and update the rotated pane and map coordinates.
|
|
1143
|
+
*
|
|
1144
|
+
* @param {number} theta Bearing in degrees.
|
|
1145
|
+
* @param {L.LatLng} [mapCenter] Center to preserve while applying the change.
|
|
1146
|
+
*
|
|
1147
|
+
* @fires L.Map#rotate
|
|
1148
|
+
*
|
|
1149
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1150
|
+
*/
|
|
1151
|
+
_setBearing(theta, mapCenter) {
|
|
1152
|
+
if (!mapCenter && this._loaded) {
|
|
1153
|
+
mapCenter = this.getCenter();
|
|
1154
|
+
}
|
|
750
1155
|
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
1156
|
this._pivot = center;
|
|
753
1157
|
this._bearing = bearing;
|
|
754
1158
|
this._rotatePanePos = newPos;
|
|
1159
|
+
if (this._pitch) {
|
|
1160
|
+
this._rotatePane._leaflet_pos = oldPos;
|
|
1161
|
+
this._updatePitchTransform();
|
|
1162
|
+
} else {
|
|
1163
|
+
L.DomUtil.setPosition(this._rotatePane, oldPos, bearing, center);
|
|
1164
|
+
}
|
|
1165
|
+
if (mapCenter) {
|
|
1166
|
+
this._pixelOrigin = this.project(mapCenter, this._zoom).subtract(this.containerPointToLayerPoint(this.getSize().divideBy(2)));
|
|
1167
|
+
delete this._lastCenter;
|
|
1168
|
+
this.fire("move");
|
|
1169
|
+
}
|
|
755
1170
|
this.fire("rotate");
|
|
756
1171
|
},
|
|
757
1172
|
/**
|
|
758
|
-
*
|
|
1173
|
+
* Transform a point with a projective matrix, accepting DOMMatrix or its 2D inverse.
|
|
759
1174
|
*
|
|
760
|
-
* @
|
|
1175
|
+
* @param {L.Point|number[]} point Input pixel coordinate.
|
|
1176
|
+
* @param {DOMMatrix|Object} matrix Forward or inverse transformation matrix.
|
|
761
1177
|
*
|
|
762
|
-
* @
|
|
1178
|
+
* @returns {L.Point} Transformed pixel coordinate.
|
|
1179
|
+
*
|
|
1180
|
+
* @since leaflet-rotate (0.5.0)
|
|
763
1181
|
*/
|
|
764
|
-
|
|
765
|
-
|
|
1182
|
+
_transformPitchPoint(point, matrix) {
|
|
1183
|
+
point = toPoint(point);
|
|
1184
|
+
if (!(matrix instanceof DOMMatrix)) {
|
|
1185
|
+
const denominator = matrix.g * point.x + matrix.h * point.y + matrix.i;
|
|
1186
|
+
return new L.Point(
|
|
1187
|
+
(matrix.a * point.x + matrix.b * point.y + matrix.c) / denominator,
|
|
1188
|
+
(matrix.d * point.x + matrix.e * point.y + matrix.f) / denominator
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
const transformed = new DOMPoint(point.x, point.y).matrixTransform(matrix);
|
|
1192
|
+
return new L.Point(transformed.x / transformed.w, transformed.y / transformed.w);
|
|
766
1193
|
},
|
|
767
1194
|
/**
|
|
768
|
-
*
|
|
769
|
-
* exist already, then returns it. The pane is created as a child of
|
|
770
|
-
* `container`, or as a child of the main map pane if not set.
|
|
1195
|
+
* Apply the current pitch/bearing CSS transform and cache its forward and inverse matrices.
|
|
771
1196
|
*
|
|
772
|
-
* @
|
|
773
|
-
* @param {HTMLElement} [container] parent element
|
|
774
|
-
* @returns {HTMLElement} pane container
|
|
1197
|
+
* @since leaflet-rotate (0.5.0)
|
|
775
1198
|
*/
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1199
|
+
_updatePitchTransform() {
|
|
1200
|
+
if (!this._pitch || !this._rotatePane) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
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)`;
|
|
1204
|
+
this._rotatePane.style.transformOrigin = "0 0";
|
|
1205
|
+
this._rotatePane.style.transform = transform;
|
|
1206
|
+
const matrix = this._pitchTransform = new DOMMatrix(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);
|
|
1207
|
+
this._pitchInverseTransform = {
|
|
1208
|
+
a: (e * i - f * h) / determinant,
|
|
1209
|
+
b: (c * h - b * i) / determinant,
|
|
1210
|
+
c: (b * f - c * e) / determinant,
|
|
1211
|
+
d: (f * g - d * i) / determinant,
|
|
1212
|
+
e: (a * i - c * g) / determinant,
|
|
1213
|
+
f: (c * d - a * f) / determinant,
|
|
1214
|
+
g: (d * h - e * g) / determinant,
|
|
1215
|
+
h: (b * g - a * h) / determinant,
|
|
1216
|
+
i: (a * e - b * d) / determinant
|
|
1217
|
+
};
|
|
1218
|
+
this._updatePitchSky();
|
|
1219
|
+
},
|
|
1220
|
+
/**
|
|
1221
|
+
* Fill the area above the far plane (where no tiles are loaded) with a sky gradient.
|
|
1222
|
+
* Style it through the `.leaflet-pitch-sky` class.
|
|
1223
|
+
*
|
|
1224
|
+
* @since leaflet-rotate (0.5.1)
|
|
1225
|
+
*/
|
|
1226
|
+
_updatePitchSky() {
|
|
1227
|
+
const farY = this._pitch && this._pitchInverseTransform ? this._getPitchFarY() : -Infinity;
|
|
1228
|
+
if (farY <= 0) {
|
|
1229
|
+
if (this._pitchSky) {
|
|
1230
|
+
this._pitchSky.style.display = "none";
|
|
1231
|
+
}
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (!this._pitchSky) {
|
|
1235
|
+
this._pitchSky = L.DomUtil.create("div", "leaflet-pitch-sky", this._container);
|
|
1236
|
+
Object.assign(this._pitchSky.style, {
|
|
1237
|
+
position: "absolute",
|
|
1238
|
+
left: 0,
|
|
1239
|
+
top: 0,
|
|
1240
|
+
width: "100%",
|
|
1241
|
+
zIndex: 0,
|
|
1242
|
+
pointerEvents: "none",
|
|
1243
|
+
background: "linear-gradient(#9ec5e8, #dfe9f0)"
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
this._pitchSky.style.display = "";
|
|
1247
|
+
this._pitchSky.style.height = `${Math.ceil(farY) + 1}px`;
|
|
1248
|
+
},
|
|
1249
|
+
/**
|
|
1250
|
+
* Set the map pitch in degrees. Values are clamped to the range 0–85.
|
|
1251
|
+
*
|
|
1252
|
+
* @param {number} pitch Pitch angle in degrees; clamped to [0, 85].
|
|
1253
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
1254
|
+
*
|
|
1255
|
+
* @returns {L.Map} This map instance.
|
|
1256
|
+
*
|
|
1257
|
+
* @fires L.Map#pitch
|
|
1258
|
+
*
|
|
1259
|
+
* @since leaflet-rotate (0.5.0)
|
|
1260
|
+
*/
|
|
1261
|
+
setPitch(pitch, options) {
|
|
1262
|
+
if (options && options.animate) {
|
|
1263
|
+
this._animatePitchBearing(pitch, this.getBearing(), options);
|
|
1264
|
+
return this;
|
|
1265
|
+
}
|
|
1266
|
+
this._stopPitchBearingAnimation();
|
|
1267
|
+
this._setPitchAndBearing(pitch, this.getBearing());
|
|
1268
|
+
return this;
|
|
1269
|
+
},
|
|
1270
|
+
/**
|
|
1271
|
+
* Update pitch and bearing together, preserving the current map center.
|
|
1272
|
+
* Used by the Ctrl+drag handler to avoid applying two separate transforms.
|
|
1273
|
+
*
|
|
1274
|
+
* @param {number} pitch Pitch angle in degrees.
|
|
1275
|
+
* @param {number} bearing Bearing angle in degrees.
|
|
1276
|
+
*
|
|
1277
|
+
* @fires L.Map#pitch
|
|
1278
|
+
* @fires L.Map#rotate
|
|
1279
|
+
*
|
|
1280
|
+
* @since leaflet-rotate (0.5.0)
|
|
1281
|
+
*/
|
|
1282
|
+
_setPitchAndBearing(pitch, bearing) {
|
|
1283
|
+
if (!this._rotate) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
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;
|
|
1287
|
+
if (!pitchChanged && clampedBearing === this._bearing) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const mapCenter = this._loaded ? this.getCenter() : void 0;
|
|
1291
|
+
if (pitchChanged) {
|
|
1292
|
+
this._pitch = clampedPitch;
|
|
1293
|
+
this._rotatePane.style.transformOrigin = L.version.startsWith("2.") ? "0 0" : "";
|
|
1294
|
+
}
|
|
1295
|
+
this._setBearing(bearing, mapCenter);
|
|
1296
|
+
if (pitchChanged) {
|
|
1297
|
+
if (!this._pitch) {
|
|
1298
|
+
this._updatePitchSky();
|
|
1299
|
+
}
|
|
1300
|
+
this.fire("pitch");
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
/**
|
|
1304
|
+
* Get the current map pitch in degrees.
|
|
1305
|
+
*
|
|
1306
|
+
* @returns {number} Pitch angle in degrees.
|
|
1307
|
+
*
|
|
1308
|
+
* @since leaflet-rotate (0.5.0)
|
|
1309
|
+
*/
|
|
1310
|
+
getPitch() {
|
|
1311
|
+
return (this._pitch || 0) * (180 / Math.PI);
|
|
1312
|
+
},
|
|
1313
|
+
/**
|
|
1314
|
+
* Zoom while keeping the given point fixed on screen, also when the map is pitched.
|
|
1315
|
+
*
|
|
1316
|
+
* Leaflet interpolates in container space, which is not linear under perspective;
|
|
1317
|
+
* interpolating in layer space keeps the point under the cursor.
|
|
1318
|
+
*
|
|
1319
|
+
* @param {L.LatLng|L.Point} latlng Geographic point or container point to keep fixed.
|
|
1320
|
+
* @param {number} zoom Target zoom level.
|
|
1321
|
+
* @param {Object} [options] Zoom options.
|
|
1322
|
+
*
|
|
1323
|
+
* @returns {L.Map} This map instance.
|
|
1324
|
+
*
|
|
1325
|
+
* @since leaflet-rotate (0.5.1)
|
|
1326
|
+
*/
|
|
1327
|
+
setZoomAround(latlng, zoom, options) {
|
|
1328
|
+
if (!this._pitch) {
|
|
1329
|
+
return mapProto.setZoomAround.apply(this, arguments);
|
|
1330
|
+
}
|
|
1331
|
+
const scale = this.getZoomScale(zoom), size = this.getSize(), center = this.containerPointToLayerPoint(size.divideBy(2)), containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng), point = this.containerPointToLayerPoint(containerPoint), newCenter = this.layerPointToLatLng(center.add(point.subtract(center).multiplyBy(1 - 1 / scale)));
|
|
1332
|
+
options = Object.assign({}, options);
|
|
1333
|
+
if (options.animate === void 0 && new L.Bounds(new L.Point(0, 0), size).contains(containerPoint)) {
|
|
1334
|
+
options.animate = true;
|
|
1335
|
+
}
|
|
1336
|
+
return this.setView(newCenter, zoom, { zoom: options });
|
|
1337
|
+
},
|
|
1338
|
+
/**
|
|
1339
|
+
* Get the current map bearing. Enable the `rotate` map option before calling.
|
|
1340
|
+
*
|
|
1341
|
+
* @returns {number} Bearing in degrees, normalized to [0, 360).
|
|
1342
|
+
*
|
|
1343
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1344
|
+
*/
|
|
1345
|
+
getBearing() {
|
|
1346
|
+
return this._bearing * (180 / Math.PI);
|
|
1347
|
+
},
|
|
1348
|
+
/**
|
|
1349
|
+
* Creates a new [map pane](#map-pane) with the given name if it doesn't
|
|
1350
|
+
* exist already, then returns it. The pane is created as a child of
|
|
1351
|
+
* `container`, or as a child of the main map pane if not set.
|
|
1352
|
+
*
|
|
1353
|
+
* @param {String} name leaflet pane
|
|
1354
|
+
* @param {HTMLElement} [container] parent element
|
|
1355
|
+
*
|
|
1356
|
+
* @returns {HTMLElement} pane container
|
|
1357
|
+
*
|
|
1358
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1359
|
+
*/
|
|
1360
|
+
// createPane(name, container) {
|
|
1361
|
+
// if (!this._rotate || name == 'mapPane') {
|
|
1362
|
+
// return mapProto.createPane.apply(this, arguments);
|
|
1363
|
+
// }
|
|
1364
|
+
// // init "rotatePane"
|
|
1365
|
+
// if (!this._rotatePane) {
|
|
1366
|
+
// // this._pivot = this.getSize().divideBy(2);
|
|
1367
|
+
// this._rotatePane = mapProto.createPane.call(this, 'rotatePane', this._mapPane);
|
|
1368
|
+
// L.DomUtil.setPosition(this._rotatePane, new L.Point(0, 0), this._bearing, this._pivot);
|
|
1369
|
+
// }
|
|
1370
|
+
// return mapProto.createPane.call(this, name, container || this._rotatePane);
|
|
1371
|
+
// },
|
|
1372
|
+
/**
|
|
1373
|
+
* Panes are DOM elements used to control the ordering of layers on
|
|
1374
|
+
* the map. You can access panes with [`map.getPane`](#map-getpane)
|
|
791
1375
|
* or [`map.getPanes`](#map-getpanes) methods. New panes can be created
|
|
792
1376
|
* with the [`map.createPane`](#map-createpane) method.
|
|
793
1377
|
*
|
|
@@ -801,6 +1385,12 @@ var leafletRotate = (() => {
|
|
|
801
1385
|
* - tooltipPane [HTMLElement = 650] - Pane for tooltips.
|
|
802
1386
|
* - popupPane [HTMLElement = 700] - Pane for popups.
|
|
803
1387
|
*/
|
|
1388
|
+
/**
|
|
1389
|
+
* Create the map's panes, placing tile/vector layers in the transformed pane
|
|
1390
|
+
* and markers/overlays in the pane that remains upright.
|
|
1391
|
+
*
|
|
1392
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1393
|
+
*/
|
|
804
1394
|
_initPanes() {
|
|
805
1395
|
const panes = this._panes = {};
|
|
806
1396
|
this._paneRenderers = {};
|
|
@@ -839,13 +1429,15 @@ var leafletRotate = (() => {
|
|
|
839
1429
|
*
|
|
840
1430
|
* @see https://github.com/Raruto/leaflet-rotate/issues/18
|
|
841
1431
|
*
|
|
842
|
-
* @param {L.LatLng} latlng
|
|
843
|
-
* @param {Object} [options={}] padding
|
|
1432
|
+
* @param {L.LatLng} latlng Geographic point to keep visible.
|
|
1433
|
+
* @param {Object} [options={}] Padding options (`padding`, `paddingTopLeft`, `paddingBottomRight`).
|
|
844
1434
|
*
|
|
845
1435
|
* @returns {L.Map} current map instance
|
|
1436
|
+
*
|
|
1437
|
+
* @since leaflet-rotate (v0.1.4)
|
|
846
1438
|
*/
|
|
847
1439
|
panInside(latlng, options) {
|
|
848
|
-
if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
|
|
1440
|
+
if (!this._rotate || !this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1) {
|
|
849
1441
|
return mapProto.panInside.apply(this, arguments);
|
|
850
1442
|
}
|
|
851
1443
|
options = options || {};
|
|
@@ -871,9 +1463,12 @@ var leafletRotate = (() => {
|
|
|
871
1463
|
*
|
|
872
1464
|
* @see https://github.com/fnicollet/Leaflet/issues/7
|
|
873
1465
|
*
|
|
874
|
-
* @param {L.LatLngBounds} bounds
|
|
875
|
-
* @param {Object} [options]
|
|
876
|
-
*
|
|
1466
|
+
* @param {L.LatLngBounds} bounds Geographic bounds to fit inside the viewport.
|
|
1467
|
+
* @param {Object} [options] Pan animation options.
|
|
1468
|
+
*
|
|
1469
|
+
* @returns {L.Map} This map instance.
|
|
1470
|
+
*
|
|
1471
|
+
* @since leaflet-rotate (v0.1.0)
|
|
877
1472
|
*/
|
|
878
1473
|
// panInsideBounds(bounds, options) {
|
|
879
1474
|
// this._enforcingBounds = true;
|
|
@@ -938,14 +1533,16 @@ var leafletRotate = (() => {
|
|
|
938
1533
|
* instead returns the minimum zoom level on which the map view fits into
|
|
939
1534
|
* the given bounds in its entirety.
|
|
940
1535
|
*
|
|
941
|
-
* @param {L.LatLngBounds} bounds
|
|
942
|
-
* @param {
|
|
943
|
-
* @param {L.Point} [padding=[0,0]]
|
|
1536
|
+
* @param {L.LatLngBounds} bounds Geographic bounds to fit.
|
|
1537
|
+
* @param {boolean} [inside=false] Fit the viewport inside bounds instead of bounds inside the viewport.
|
|
1538
|
+
* @param {L.Point|number[]} [padding=[0,0]] Pixel padding around the viewport.
|
|
944
1539
|
*
|
|
945
|
-
* @returns {
|
|
1540
|
+
* @returns {number} Zoom level satisfying the fit.
|
|
1541
|
+
*
|
|
1542
|
+
* @since leaflet-rotate (v0.1.0)
|
|
946
1543
|
*/
|
|
947
1544
|
getBoundsZoom(bounds, inside, padding) {
|
|
948
|
-
if (!this._rotate || Math.abs(this._bearing).toFixed(1) < 0.1) {
|
|
1545
|
+
if (!this._rotate || !this._pitch && Math.abs(this._bearing).toFixed(1) < 0.1) {
|
|
949
1546
|
return mapProto.getBoundsZoom.apply(this, arguments);
|
|
950
1547
|
}
|
|
951
1548
|
bounds = bounds instanceof L.LatLngBounds ? bounds : new L.LatLngBounds(bounds);
|
|
@@ -968,19 +1565,26 @@ var leafletRotate = (() => {
|
|
|
968
1565
|
// return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
|
|
969
1566
|
// },
|
|
970
1567
|
/**
|
|
971
|
-
*
|
|
1568
|
+
* Return the pixel offset from the current center to the given geographic point.
|
|
1569
|
+
*
|
|
1570
|
+
* @param {L.LatLng} latlng Geographic point.
|
|
1571
|
+
*
|
|
1572
|
+
* @returns {L.Point} Offset in container pixels (the map pane is panned in screen space), adjusted for bearing and pitch.
|
|
972
1573
|
*
|
|
973
|
-
* @
|
|
1574
|
+
* @since leaflet-rotate (v0.1.0)
|
|
974
1575
|
*/
|
|
975
1576
|
_getCenterOffset(latlng) {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
centerOffset = centerOffset.rotate(this._bearing);
|
|
1577
|
+
if (!this._rotate) {
|
|
1578
|
+
return mapProto._getCenterOffset.apply(this, arguments);
|
|
979
1579
|
}
|
|
980
|
-
return
|
|
1580
|
+
return this.latLngToContainerPoint(latlng)._subtract(this.getSize()._divideBy(2));
|
|
981
1581
|
},
|
|
982
1582
|
/**
|
|
983
|
-
*
|
|
1583
|
+
* Return the current position of the rotated pane.
|
|
1584
|
+
*
|
|
1585
|
+
* @returns {L.Point} Rotated-pane position in map-pane coordinates.
|
|
1586
|
+
*
|
|
1587
|
+
* @since leaflet-rotate (v0.1.0)
|
|
984
1588
|
*/
|
|
985
1589
|
_getRotatePanePos() {
|
|
986
1590
|
return this._rotatePanePos || new L.Point(0, 0);
|
|
@@ -989,15 +1593,31 @@ var leafletRotate = (() => {
|
|
|
989
1593
|
// const topLeft = this._getNewPixelOrigin(center, zoom);
|
|
990
1594
|
// return this.project(latlng, zoom)._subtract(topLeft);
|
|
991
1595
|
//},
|
|
1596
|
+
/**
|
|
1597
|
+
* Calculate the pixel origin for a target view while preserving bearing-aware alignment.
|
|
1598
|
+
*
|
|
1599
|
+
* @param {L.LatLng} center Target center.
|
|
1600
|
+
* @param {number} zoom Target zoom level.
|
|
1601
|
+
*
|
|
1602
|
+
* @returns {L.Point} New pixel origin.
|
|
1603
|
+
*
|
|
1604
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1605
|
+
*/
|
|
992
1606
|
_getNewPixelOrigin(center, zoom) {
|
|
993
|
-
if (!this._rotate
|
|
1607
|
+
if (!this._rotate) {
|
|
994
1608
|
return mapProto._getNewPixelOrigin.apply(this, arguments);
|
|
995
1609
|
}
|
|
996
|
-
|
|
997
|
-
return this.project(center, zoom).rotate(this._bearing)._subtract(viewHalf)._add(this._getMapPanePos())._add(this._getRotatePanePos()).rotate(-this._bearing)._round();
|
|
1610
|
+
return this.project(center, zoom)._subtract(this.containerPointToLayerPoint(this.getSize()._divideBy(2)))._round();
|
|
998
1611
|
},
|
|
999
1612
|
/**
|
|
1000
|
-
*
|
|
1613
|
+
* Calculate the pixel bounds needed to cover the transformed viewport.
|
|
1614
|
+
*
|
|
1615
|
+
* @param {L.LatLng} [center=this.getCenter()] Target center.
|
|
1616
|
+
* @param {number} [zoom=this.getZoom()] Target zoom level.
|
|
1617
|
+
*
|
|
1618
|
+
* @returns {L.Bounds} Pixel bounds in projected map coordinates.
|
|
1619
|
+
*
|
|
1620
|
+
* @since leaflet-rotate (v0.2.0)
|
|
1001
1621
|
*
|
|
1002
1622
|
* @see src\layer\tile\GridLayer::_getTiledPixelBounds()
|
|
1003
1623
|
*/
|
|
@@ -1007,18 +1627,18 @@ var leafletRotate = (() => {
|
|
|
1007
1627
|
if (!this._rotate && mapProto._getNewPixelBounds) {
|
|
1008
1628
|
return mapProto._getNewPixelBounds.apply(this, arguments);
|
|
1009
1629
|
}
|
|
1010
|
-
const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(), scale = this.getZoomScale(mapZoom, zoom), pixelCenter = this.project(center, zoom).floor(),
|
|
1011
|
-
|
|
1012
|
-
this.containerPointToLayerPoint(
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1630
|
+
const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(), scale = this.getZoomScale(mapZoom, zoom), pixelCenter = this.project(center, zoom).floor(), [min, max] = this._getPitchedContainerBounds(0), corners = [[min.x, min.y], [max.x, min.y], [min.x, max.y], [max.x, max.y]].map((point) => this.containerPointToLayerPoint(point));
|
|
1631
|
+
if (this._pitch) {
|
|
1632
|
+
const viewCenter = this.containerPointToLayerPoint(this.getSize()._divideBy(2));
|
|
1633
|
+
return new L.Bounds(corners.map((point) => pixelCenter.add(point.subtract(viewCenter).divideBy(scale))));
|
|
1634
|
+
}
|
|
1635
|
+
const halfSize = new L.Bounds(corners.map((point) => point.floor())).getSize().divideBy(scale * 2);
|
|
1016
1636
|
return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
|
|
1017
1637
|
},
|
|
1018
1638
|
/**
|
|
1019
|
-
* @since leaflet-rotate (v0.2)
|
|
1639
|
+
* @since leaflet-rotate (v0.2.0)
|
|
1020
1640
|
*
|
|
1021
|
-
* @
|
|
1641
|
+
* @returns {L.Point} Map pivot point in pixel coordinates.
|
|
1022
1642
|
*/
|
|
1023
1643
|
_getPixelCenter() {
|
|
1024
1644
|
if (!this._rotate && mapProto._getPixelCenter) {
|
|
@@ -1027,15 +1647,19 @@ var leafletRotate = (() => {
|
|
|
1027
1647
|
return this.getSize()._divideBy(2)._subtract(this._getMapPanePos());
|
|
1028
1648
|
},
|
|
1029
1649
|
/**
|
|
1030
|
-
* @since leaflet-rotate (v0.2)
|
|
1031
|
-
*
|
|
1032
1650
|
* @see src\layer\vector\Renderer::_update()
|
|
1651
|
+
*
|
|
1652
|
+
* @param {number} padding Bounds padding as a fraction of map size.
|
|
1653
|
+
*
|
|
1654
|
+
* @returns {L.Bounds} Padded bounds in layer-point coordinates.
|
|
1655
|
+
*
|
|
1656
|
+
* @since leaflet-rotate (v0.2.0)
|
|
1033
1657
|
*/
|
|
1034
1658
|
_getPaddedPixelBounds(padding) {
|
|
1035
1659
|
if (!this._rotate && mapProto._getPaddedPixelBounds) {
|
|
1036
1660
|
return mapProto._getPaddedPixelBounds.apply(this, arguments);
|
|
1037
1661
|
}
|
|
1038
|
-
const
|
|
1662
|
+
const [padMin, padMax] = this._getPitchedContainerBounds(padding);
|
|
1039
1663
|
return new L.Bounds([
|
|
1040
1664
|
this.containerPointToLayerPoint([padMin.x, padMin.y]).floor(),
|
|
1041
1665
|
this.containerPointToLayerPoint([padMin.x, padMax.y]).floor(),
|
|
@@ -1043,6 +1667,15 @@ var leafletRotate = (() => {
|
|
|
1043
1667
|
this.containerPointToLayerPoint([padMax.x, padMax.y]).floor()
|
|
1044
1668
|
]);
|
|
1045
1669
|
},
|
|
1670
|
+
/**
|
|
1671
|
+
* Build and fire Leaflet's location-found event, including the device heading.
|
|
1672
|
+
*
|
|
1673
|
+
* @param {GeolocationPosition} pos Browser geolocation result.
|
|
1674
|
+
*
|
|
1675
|
+
* @fires L.Map#locationfound
|
|
1676
|
+
*
|
|
1677
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1678
|
+
*/
|
|
1046
1679
|
_handleGeolocationResponse(pos) {
|
|
1047
1680
|
if (!this._container._leaflet_id) {
|
|
1048
1681
|
return;
|
|
@@ -1070,6 +1703,7 @@ var leafletRotate = (() => {
|
|
|
1070
1703
|
* @see https://github.com/ronikar/Leaflet/blob/5c480ef959b947c3beed7065425a5a36c486262b/src/geo/LatLngBounds.js#L253-L264
|
|
1071
1704
|
*
|
|
1072
1705
|
* @param {L.Bounds} points
|
|
1706
|
+
*
|
|
1073
1707
|
* @returns {L.Bounds}
|
|
1074
1708
|
*/
|
|
1075
1709
|
// toCircumscribedBounds(points) {
|
|
@@ -1084,6 +1718,11 @@ var leafletRotate = (() => {
|
|
|
1084
1718
|
|
|
1085
1719
|
// src/map/handler/CompassBearing.js
|
|
1086
1720
|
L.Map.CompassBearing = L.Handler.extend({
|
|
1721
|
+
/**
|
|
1722
|
+
* @param {L.Map} map Map whose bearing follows device orientation.
|
|
1723
|
+
*
|
|
1724
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1725
|
+
*/
|
|
1087
1726
|
initialize(map) {
|
|
1088
1727
|
this._map = map;
|
|
1089
1728
|
if ("ondeviceorientationabsolute" in window) {
|
|
@@ -1093,19 +1732,37 @@ var leafletRotate = (() => {
|
|
|
1093
1732
|
}
|
|
1094
1733
|
this._throttled = L.Util.throttle(this._onDeviceOrientation, 100, this);
|
|
1095
1734
|
},
|
|
1735
|
+
/**
|
|
1736
|
+
* Subscribe to the supported device-orientation event, if available.
|
|
1737
|
+
* On iOS, permission is requested first (call `enable()` from a user gesture).
|
|
1738
|
+
*
|
|
1739
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1740
|
+
*/
|
|
1096
1741
|
addHooks() {
|
|
1097
1742
|
if (this._map._rotate && this.__deviceOrientationEvent) {
|
|
1098
1743
|
L.DomEvent.on(window, this.__deviceOrientationEvent, this._throttled, this);
|
|
1744
|
+
const DOE = window.DeviceOrientationEvent;
|
|
1745
|
+
if (DOE && DOE.requestPermission) {
|
|
1746
|
+
DOE.requestPermission().then((state) => "denied" === state && this.enabled() && this.disable()).catch(() => {
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1099
1749
|
} else {
|
|
1100
1750
|
this.disable();
|
|
1101
1751
|
}
|
|
1102
1752
|
},
|
|
1753
|
+
/**
|
|
1754
|
+
* Unsubscribe from device orientation when the handler is disabled.
|
|
1755
|
+
*
|
|
1756
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1757
|
+
*/
|
|
1103
1758
|
removeHooks() {
|
|
1104
1759
|
if (this._map._rotate && this.__deviceOrientationEvent) {
|
|
1105
1760
|
L.DomEvent.off(window, this.__deviceOrientationEvent, this._throttled, this);
|
|
1106
1761
|
}
|
|
1107
1762
|
},
|
|
1108
1763
|
/**
|
|
1764
|
+
* Apply a device orientation reading to the map bearing.
|
|
1765
|
+
*
|
|
1109
1766
|
* `DeviceOrientationEvent.absolute` - Indicates whether the device is providing absolute
|
|
1110
1767
|
* orientation values (relatives to Magnetic North) or
|
|
1111
1768
|
* using some arbitrary frame determined by the device.
|
|
@@ -1122,6 +1779,10 @@ var leafletRotate = (() => {
|
|
|
1122
1779
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/absolute
|
|
1123
1780
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/DeviceOrientationEvent/alpha
|
|
1124
1781
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/Window/orientation
|
|
1782
|
+
*
|
|
1783
|
+
* @param {DeviceOrientationEvent} e Orientation reading from the browser.
|
|
1784
|
+
*
|
|
1785
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1125
1786
|
*/
|
|
1126
1787
|
_onDeviceOrientation(e) {
|
|
1127
1788
|
let angle = e.webkitCompassHeading != null ? e.webkitCompassHeading : e.alpha;
|
|
@@ -1150,11 +1811,18 @@ var leafletRotate = (() => {
|
|
|
1150
1811
|
*
|
|
1151
1812
|
* @type {Boolean}
|
|
1152
1813
|
*
|
|
1814
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1815
|
+
*
|
|
1153
1816
|
* @see https://developer.mozilla.org/docs/Web/API/MutationObserver
|
|
1154
1817
|
*/
|
|
1155
1818
|
trackContainerMutation: false
|
|
1156
1819
|
});
|
|
1157
1820
|
L.Map.ContainerMutation = L.Handler.extend({
|
|
1821
|
+
/**
|
|
1822
|
+
* Start observing the map container's style attribute.
|
|
1823
|
+
*
|
|
1824
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1825
|
+
*/
|
|
1158
1826
|
addHooks() {
|
|
1159
1827
|
if (!this._observer) {
|
|
1160
1828
|
this._observer = new MutationObserver(L.Util.bind(this._map.invalidateSize, this._map));
|
|
@@ -1167,6 +1835,11 @@ var leafletRotate = (() => {
|
|
|
1167
1835
|
attributeFilter: ["style"]
|
|
1168
1836
|
});
|
|
1169
1837
|
},
|
|
1838
|
+
/**
|
|
1839
|
+
* Stop observing container mutations.
|
|
1840
|
+
*
|
|
1841
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1842
|
+
*/
|
|
1170
1843
|
removeHooks() {
|
|
1171
1844
|
this._observer.disconnect();
|
|
1172
1845
|
}
|
|
@@ -1183,32 +1856,65 @@ var leafletRotate = (() => {
|
|
|
1183
1856
|
}
|
|
1184
1857
|
L.Map.mergeOptions({
|
|
1185
1858
|
/**
|
|
1186
|
-
*
|
|
1187
|
-
* zoom beyond min/max zoom and then bounce back
|
|
1188
|
-
* when pinch-zooming.
|
|
1859
|
+
* Whether pinch zoom may temporarily exceed min/max zoom and bounce back.
|
|
1189
1860
|
*
|
|
1190
1861
|
* @type {Boolean}
|
|
1862
|
+
*
|
|
1863
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1191
1864
|
*/
|
|
1192
1865
|
bounceAtZoomLimits: true
|
|
1193
1866
|
});
|
|
1194
1867
|
L.Map.TouchGestures = L.Handler.extend({
|
|
1868
|
+
/**
|
|
1869
|
+
* Initialize the shared handler from the map's touch zoom/rotation options.
|
|
1870
|
+
*
|
|
1871
|
+
* @param {L.Map} map Map whose touch gestures are handled.
|
|
1872
|
+
*
|
|
1873
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1874
|
+
*/
|
|
1195
1875
|
initialize(map) {
|
|
1196
1876
|
this._map = map;
|
|
1197
1877
|
this.rotate = !!this._map.options.touchRotate;
|
|
1198
1878
|
this.zoom = !!this._map.options.touchZoom;
|
|
1879
|
+
this.pitch = !!this._map.options.touchPitch;
|
|
1199
1880
|
},
|
|
1881
|
+
/**
|
|
1882
|
+
* Listen for the start of two-finger gestures on the map container.
|
|
1883
|
+
*
|
|
1884
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1885
|
+
*/
|
|
1200
1886
|
addHooks() {
|
|
1201
1887
|
L.DomEvent.on(this._map._container, "touchstart", this._onTouchStart, this);
|
|
1202
1888
|
},
|
|
1889
|
+
/**
|
|
1890
|
+
* Remove the touch-start listener.
|
|
1891
|
+
*
|
|
1892
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1893
|
+
*/
|
|
1203
1894
|
removeHooks() {
|
|
1204
1895
|
L.DomEvent.off(this._map._container, "touchstart", this._onTouchStart, this);
|
|
1205
1896
|
},
|
|
1897
|
+
/**
|
|
1898
|
+
* Record initial touch points and start tracking a two-finger gesture.
|
|
1899
|
+
*
|
|
1900
|
+
* @param {TouchEvent} e Touch-start event.
|
|
1901
|
+
*
|
|
1902
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1903
|
+
*/
|
|
1206
1904
|
_onTouchStart(e) {
|
|
1207
1905
|
const map = this._map;
|
|
1208
|
-
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating) {
|
|
1906
|
+
if (!e.touches || e.touches.length !== 2 || map._animatingZoom || this._zooming || this._rotating || this._gesture) {
|
|
1209
1907
|
return;
|
|
1210
1908
|
}
|
|
1211
1909
|
const p1 = touchToContainerPoint(map, e.touches[0]), p2 = touchToContainerPoint(map, e.touches[1]), vector = p1.subtract(p2);
|
|
1910
|
+
const canPitch = this.pitch && map._rotate && Math.abs(vector.x) > Math.abs(vector.y);
|
|
1911
|
+
if (!canPitch && !this.zoom && !this.rotate) {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
this._gesture = canPitch ? "pending" : "pinch";
|
|
1915
|
+
this._startP1 = p1;
|
|
1916
|
+
this._startP2 = p2;
|
|
1917
|
+
map._stopPitchBearingAnimation();
|
|
1212
1918
|
this._centerPoint = map.getSize()._divideBy(2);
|
|
1213
1919
|
this._startLatLng = map.containerPointToLatLng(this._centerPoint);
|
|
1214
1920
|
if (this.zoom) {
|
|
@@ -1236,12 +1942,44 @@ var leafletRotate = (() => {
|
|
|
1236
1942
|
L.DomEvent.on(document, "touchmove", this._onTouchMove, this).on(document, "touchend touchcancel", this._onTouchEnd, this);
|
|
1237
1943
|
L.DomEvent.preventDefault(e);
|
|
1238
1944
|
},
|
|
1945
|
+
/**
|
|
1946
|
+
* Update bearing and/or zoom while the two touch points move.
|
|
1947
|
+
*
|
|
1948
|
+
* @param {TouchEvent} e Touch-move event.
|
|
1949
|
+
*
|
|
1950
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1951
|
+
*/
|
|
1239
1952
|
_onTouchMove(e) {
|
|
1240
|
-
if (!e.touches || e.touches.length !== 2 || !
|
|
1953
|
+
if (!e.touches || e.touches.length !== 2 || !this._gesture) {
|
|
1241
1954
|
return;
|
|
1242
1955
|
}
|
|
1243
1956
|
const map = this._map, p1 = touchToContainerPoint(map, e.touches[0]), p2 = touchToContainerPoint(map, e.touches[1]), vector = p1.subtract(p2), scale = p1.distanceTo(p2) / this._startDist;
|
|
1244
1957
|
let delta;
|
|
1958
|
+
if (this._gesture === "pending") {
|
|
1959
|
+
const d1 = p1.subtract(this._startP1), d2 = p2.subtract(this._startP2);
|
|
1960
|
+
if (Math.max(d1.x * d1.x + d1.y * d1.y, d2.x * d2.x + d2.y * d2.y) < 25) {
|
|
1961
|
+
L.DomEvent.preventDefault(e);
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
const vertical = (d) => Math.abs(d.y) > Math.abs(d.x);
|
|
1965
|
+
if (vertical(d1) && vertical(d2) && Math.sign(d1.y) === Math.sign(d2.y)) {
|
|
1966
|
+
this._gesture = "pitch";
|
|
1967
|
+
this._lastPitchY = (this._startP1.y + this._startP2.y) / 2;
|
|
1968
|
+
this._zooming = this._rotating = false;
|
|
1969
|
+
} else {
|
|
1970
|
+
this._gesture = "pinch";
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
if (this._gesture === "pitch") {
|
|
1974
|
+
const y = (p1.y + p2.y) / 2;
|
|
1975
|
+
map._setPitchAndBearing(map.getPitch() - (y - this._lastPitchY) * 0.5, map.getBearing());
|
|
1976
|
+
this._lastPitchY = y;
|
|
1977
|
+
L.DomEvent.preventDefault(e);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (!(this._zooming || this._rotating)) {
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1245
1983
|
if (this._rotating) {
|
|
1246
1984
|
const theta = Math.atan(vector.x / vector.y);
|
|
1247
1985
|
let bearingDelta = (theta - this._startTheta) * (180 / Math.PI);
|
|
@@ -1267,8 +2005,8 @@ var leafletRotate = (() => {
|
|
|
1267
2005
|
if (scale === 1 && delta.x === 0 && delta.y === 0) {
|
|
1268
2006
|
return;
|
|
1269
2007
|
}
|
|
1270
|
-
const
|
|
1271
|
-
this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(
|
|
2008
|
+
const mapDelta = map._pitch ? map.containerPointToLayerPoint(this._centerPoint.add(delta)).subtract(map.containerPointToLayerPoint(this._centerPoint)) : delta.rotate(-map.getBearing() * (Math.PI / 180));
|
|
2009
|
+
this._center = map.unproject(map.project(this._pinchStartLatLng).subtract(mapDelta));
|
|
1272
2010
|
}
|
|
1273
2011
|
}
|
|
1274
2012
|
if (!this._moved) {
|
|
@@ -1286,9 +2024,18 @@ var leafletRotate = (() => {
|
|
|
1286
2024
|
}
|
|
1287
2025
|
L.DomEvent.preventDefault(e);
|
|
1288
2026
|
},
|
|
2027
|
+
/**
|
|
2028
|
+
* Finish the gesture, settle the zoom and remove document listeners.
|
|
2029
|
+
*
|
|
2030
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2031
|
+
*/
|
|
1289
2032
|
_onTouchEnd() {
|
|
1290
|
-
|
|
2033
|
+
const gesture = this._gesture;
|
|
2034
|
+
delete this._gesture;
|
|
2035
|
+
if (gesture === "pitch" || gesture === "pending" || !this._moved || !(this._zooming || this._rotating)) {
|
|
1291
2036
|
this._zooming = false;
|
|
2037
|
+
this._rotating = false;
|
|
2038
|
+
L.DomEvent.off(document, "touchmove", this._onTouchMove, this).off(document, "touchend touchcancel", this._onTouchEnd, this);
|
|
1292
2039
|
return;
|
|
1293
2040
|
}
|
|
1294
2041
|
this._zooming = false;
|
|
@@ -1311,50 +2058,279 @@ var leafletRotate = (() => {
|
|
|
1311
2058
|
* Whether the map can be rotated with a two-finger rotation gesture
|
|
1312
2059
|
*
|
|
1313
2060
|
* @type {Boolean}
|
|
2061
|
+
*
|
|
2062
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1314
2063
|
*/
|
|
1315
2064
|
touchRotate: false
|
|
1316
2065
|
});
|
|
1317
2066
|
L.Map.TouchRotate = L.Handler.extend({
|
|
2067
|
+
/**
|
|
2068
|
+
* Enable rotation handling in the map's shared touch gesture handler.
|
|
2069
|
+
*
|
|
2070
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2071
|
+
*/
|
|
1318
2072
|
addHooks() {
|
|
1319
2073
|
this._map.touchGestures.enable();
|
|
1320
2074
|
this._map.touchGestures.rotate = true;
|
|
1321
2075
|
},
|
|
2076
|
+
/**
|
|
2077
|
+
* Disable rotation handling without affecting pinch zoom.
|
|
2078
|
+
*
|
|
2079
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2080
|
+
*/
|
|
1322
2081
|
removeHooks() {
|
|
1323
2082
|
this._map.touchGestures.rotate = false;
|
|
1324
2083
|
}
|
|
1325
2084
|
});
|
|
1326
2085
|
L.Map.addInitHook("addHandler", "touchRotate", L.Map.TouchRotate);
|
|
1327
2086
|
|
|
2087
|
+
// src/map/handler/TouchPitch.js
|
|
2088
|
+
L.Map.mergeOptions({
|
|
2089
|
+
/**
|
|
2090
|
+
* Whether the map can be pitched by dragging two fingers vertically
|
|
2091
|
+
*
|
|
2092
|
+
* @type {Boolean}
|
|
2093
|
+
*
|
|
2094
|
+
* @since leaflet-rotate (0.5.1)
|
|
2095
|
+
*/
|
|
2096
|
+
touchPitch: false
|
|
2097
|
+
});
|
|
2098
|
+
L.Map.TouchPitch = L.Handler.extend({
|
|
2099
|
+
/**
|
|
2100
|
+
* Enable pitch handling in the map's shared touch gesture handler.
|
|
2101
|
+
*
|
|
2102
|
+
* @since leaflet-rotate (0.5.1)
|
|
2103
|
+
*/
|
|
2104
|
+
addHooks() {
|
|
2105
|
+
this._map.touchGestures.enable();
|
|
2106
|
+
this._map.touchGestures.pitch = true;
|
|
2107
|
+
},
|
|
2108
|
+
/**
|
|
2109
|
+
* Disable pitch handling without affecting pinch zoom and rotation.
|
|
2110
|
+
*
|
|
2111
|
+
* @since leaflet-rotate (0.5.1)
|
|
2112
|
+
*/
|
|
2113
|
+
removeHooks() {
|
|
2114
|
+
this._map.touchGestures.pitch = false;
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
L.Map.addInitHook("addHandler", "touchPitch", L.Map.TouchPitch);
|
|
2118
|
+
|
|
1328
2119
|
// src/map/handler/ShiftKeyRotate.js
|
|
1329
2120
|
L.Map.mergeOptions({
|
|
1330
2121
|
/**
|
|
1331
|
-
* Whether the map
|
|
2122
|
+
* Whether Shift+wheel changes the map bearing.
|
|
2123
|
+
*
|
|
1332
2124
|
* @type {Boolean}
|
|
2125
|
+
*
|
|
2126
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2127
|
+
*/
|
|
2128
|
+
shiftKeyRotate: true,
|
|
2129
|
+
/**
|
|
2130
|
+
* Whether Ctrl+drag (or right-button drag) changes bearing and pitch.
|
|
2131
|
+
*
|
|
2132
|
+
* @type {Boolean}
|
|
2133
|
+
*
|
|
2134
|
+
* @since leaflet-rotate (0.5.0)
|
|
1333
2135
|
*/
|
|
1334
|
-
|
|
2136
|
+
pitchDrag: true
|
|
1335
2137
|
});
|
|
1336
2138
|
L.Map.ShiftKeyRotate = L.Handler.extend({
|
|
2139
|
+
/**
|
|
2140
|
+
* @param {L.Map} map Map controlled by this handler.
|
|
2141
|
+
*
|
|
2142
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2143
|
+
*/
|
|
2144
|
+
initialize(map) {
|
|
2145
|
+
this._map = map;
|
|
2146
|
+
this._onMouseDownBound = this._onMouseDown.bind(this);
|
|
2147
|
+
this._onMouseMoveBound = this._onMouseMove.bind(this);
|
|
2148
|
+
this._onMouseUpBound = this._onMouseUp.bind(this);
|
|
2149
|
+
this._onContextMenuBound = this._onContextMenu.bind(this);
|
|
2150
|
+
this._applyBound = this._apply.bind(this);
|
|
2151
|
+
},
|
|
2152
|
+
/**
|
|
2153
|
+
* Attach enabled wheel and pointer/mouse gesture listeners.
|
|
2154
|
+
*
|
|
2155
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2156
|
+
*/
|
|
1337
2157
|
addHooks() {
|
|
1338
|
-
|
|
1339
|
-
|
|
2158
|
+
if (this._map.options.shiftKeyRotate) {
|
|
2159
|
+
L.DomEvent.on(this._map._container, "wheel", this._handleShiftScroll, this);
|
|
2160
|
+
}
|
|
2161
|
+
if (this._map.options.pitchDrag) {
|
|
2162
|
+
this.addPitchHooks();
|
|
2163
|
+
}
|
|
1340
2164
|
},
|
|
2165
|
+
/**
|
|
2166
|
+
* Detach gesture listeners and cancel any active drag.
|
|
2167
|
+
*
|
|
2168
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2169
|
+
*/
|
|
1341
2170
|
removeHooks() {
|
|
1342
2171
|
L.DomEvent.off(this._map._container, "wheel", this._handleShiftScroll, this);
|
|
1343
|
-
this.
|
|
2172
|
+
this.removePitchHooks();
|
|
2173
|
+
},
|
|
2174
|
+
/**
|
|
2175
|
+
* Listen for the start of Ctrl+drag pitch gestures.
|
|
2176
|
+
*
|
|
2177
|
+
* @since leaflet-rotate (0.5.0)
|
|
2178
|
+
*/
|
|
2179
|
+
addPitchHooks() {
|
|
2180
|
+
if (this._pitchHooksAdded) {
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
this._pitchEvents = L.version.startsWith("2.") ? ["pointerdown", "pointermove", "pointerup pointercancel"] : ["mousedown", "mousemove", "mouseup"];
|
|
2184
|
+
this._map._container.addEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
|
|
2185
|
+
this._map._container.addEventListener("contextmenu", this._onContextMenuBound, true);
|
|
2186
|
+
this._pitchHooksAdded = true;
|
|
2187
|
+
},
|
|
2188
|
+
/**
|
|
2189
|
+
* Stop listening for pitch gestures and remove active document listeners.
|
|
2190
|
+
*
|
|
2191
|
+
* @since leaflet-rotate (0.5.0)
|
|
2192
|
+
*/
|
|
2193
|
+
removePitchHooks() {
|
|
2194
|
+
if (!this._pitchHooksAdded) {
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
this._map._container.removeEventListener(this._pitchEvents[0], this._onMouseDownBound, true);
|
|
2198
|
+
this._map._container.removeEventListener("contextmenu", this._onContextMenuBound, true);
|
|
2199
|
+
this._onMouseUp();
|
|
2200
|
+
delete this._pitchEvents;
|
|
2201
|
+
this._pitchHooksAdded = false;
|
|
1344
2202
|
},
|
|
2203
|
+
/**
|
|
2204
|
+
* Rotate the map in fixed increments while Shift is held during wheel input.
|
|
2205
|
+
*
|
|
2206
|
+
* @param {WheelEvent} e Wheel event from the map container.
|
|
2207
|
+
*
|
|
2208
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2209
|
+
*/
|
|
1345
2210
|
_handleShiftScroll(e) {
|
|
1346
2211
|
if (e.shiftKey) {
|
|
1347
2212
|
e.preventDefault();
|
|
1348
2213
|
this._map.scrollWheelZoom.disable();
|
|
1349
|
-
this._map.setBearing(this._map.
|
|
2214
|
+
this._map.setBearing(this._map.getBearing() + Math.sign(e.deltaY) * 5);
|
|
1350
2215
|
} else {
|
|
1351
2216
|
this._map.scrollWheelZoom.enable();
|
|
1352
2217
|
}
|
|
2218
|
+
},
|
|
2219
|
+
/**
|
|
2220
|
+
* Start Ctrl+drag (or right-button drag) and capture subsequent movement outside the map.
|
|
2221
|
+
*
|
|
2222
|
+
* @param {MouseEvent|PointerEvent} e Gesture start event.
|
|
2223
|
+
*
|
|
2224
|
+
* @since leaflet-rotate (0.5.0)
|
|
2225
|
+
*/
|
|
2226
|
+
_onMouseDown(e) {
|
|
2227
|
+
if (this._dragging) {
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
this._moved = false;
|
|
2231
|
+
if (!this._map._rotate || !this._map.options.pitchDrag || !(e.button === 2 || e.ctrlKey && e.button === 0)) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
const scrollWheelZoom = this._map.scrollWheelZoom;
|
|
2235
|
+
if (scrollWheelZoom && scrollWheelZoom._timer) {
|
|
2236
|
+
clearTimeout(scrollWheelZoom._timer);
|
|
2237
|
+
scrollWheelZoom._timer = null;
|
|
2238
|
+
scrollWheelZoom._delta = 0;
|
|
2239
|
+
scrollWheelZoom._startTime = null;
|
|
2240
|
+
}
|
|
2241
|
+
e.preventDefault();
|
|
2242
|
+
e.stopPropagation();
|
|
2243
|
+
e.stopImmediatePropagation();
|
|
2244
|
+
this._map._stopPitchBearingAnimation();
|
|
2245
|
+
this._dragging = true;
|
|
2246
|
+
this._lastX = e.clientX;
|
|
2247
|
+
this._lastY = e.clientY;
|
|
2248
|
+
this._pitchDelta = this._bearingDelta = 0;
|
|
2249
|
+
document.addEventListener(this._pitchEvents[1], this._onMouseMoveBound);
|
|
2250
|
+
for (const event of this._pitchEvents[2].split(" ")) {
|
|
2251
|
+
document.addEventListener(event, this._onMouseUpBound);
|
|
2252
|
+
}
|
|
2253
|
+
},
|
|
2254
|
+
/**
|
|
2255
|
+
* Accumulate pointer movement as pitch (vertical) and bearing (horizontal).
|
|
2256
|
+
* Far from the horizontal center line the bearing follows the pointer angle
|
|
2257
|
+
* around the map center.
|
|
2258
|
+
*
|
|
2259
|
+
* @param {MouseEvent|PointerEvent} e Gesture movement event.
|
|
2260
|
+
*
|
|
2261
|
+
* @since leaflet-rotate (0.5.0)
|
|
2262
|
+
*/
|
|
2263
|
+
_onMouseMove(e) {
|
|
2264
|
+
const rect = this._map._container.getBoundingClientRect(), cx = rect.left + rect.width / 2, cy = rect.top + rect.height / 2, x = e.clientX, y = e.clientY;
|
|
2265
|
+
this._pitchDelta += (this._lastY - y) * 0.5;
|
|
2266
|
+
this._bearingDelta += Math.abs(cy - this._lastY) > 100 ? (Math.atan2(y - cy, x - cx) - Math.atan2(y - cy, this._lastX - cx)) * (180 / Math.PI) : (x - this._lastX) * 0.8 * (y < cy ? 1 : -1);
|
|
2267
|
+
this._lastX = x;
|
|
2268
|
+
this._lastY = y;
|
|
2269
|
+
this._moved = true;
|
|
2270
|
+
if (!this._frame) {
|
|
2271
|
+
this._frame = requestAnimationFrame(this._applyBound);
|
|
2272
|
+
}
|
|
2273
|
+
e.preventDefault();
|
|
2274
|
+
},
|
|
2275
|
+
/**
|
|
2276
|
+
* Apply accumulated movement once per animation frame.
|
|
2277
|
+
*
|
|
2278
|
+
* @since leaflet-rotate (0.5.1)
|
|
2279
|
+
*/
|
|
2280
|
+
_apply() {
|
|
2281
|
+
this._frame = null;
|
|
2282
|
+
if (!this._pitchDelta && !this._bearingDelta) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
this._map._setPitchAndBearing(this._map.getPitch() + this._pitchDelta, this._map.getBearing() + this._bearingDelta);
|
|
2286
|
+
this._pitchDelta = this._bearingDelta = 0;
|
|
2287
|
+
},
|
|
2288
|
+
/**
|
|
2289
|
+
* Suppress the context menu that follows a right-button drag.
|
|
2290
|
+
*
|
|
2291
|
+
* @param {MouseEvent} e Context menu event.
|
|
2292
|
+
*
|
|
2293
|
+
* @since leaflet-rotate (0.5.1)
|
|
2294
|
+
*/
|
|
2295
|
+
_onContextMenu(e) {
|
|
2296
|
+
if (this._dragging || this._moved) {
|
|
2297
|
+
e.preventDefault();
|
|
2298
|
+
e.stopPropagation();
|
|
2299
|
+
}
|
|
2300
|
+
},
|
|
2301
|
+
/**
|
|
2302
|
+
* End Ctrl+drag, flush pending movement and clear document-level listeners.
|
|
2303
|
+
*
|
|
2304
|
+
* @since leaflet-rotate (0.5.0)
|
|
2305
|
+
*/
|
|
2306
|
+
_onMouseUp() {
|
|
2307
|
+
if (!this._pitchEvents || !this._dragging) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
document.removeEventListener(this._pitchEvents[1], this._onMouseMoveBound);
|
|
2311
|
+
for (const event of this._pitchEvents[2].split(" ")) {
|
|
2312
|
+
document.removeEventListener(event, this._onMouseUpBound);
|
|
2313
|
+
}
|
|
2314
|
+
cancelAnimationFrame(this._frame);
|
|
2315
|
+
if (this._map._mapPane) {
|
|
2316
|
+
this._apply();
|
|
2317
|
+
}
|
|
2318
|
+
this._frame = null;
|
|
2319
|
+
this._dragging = false;
|
|
2320
|
+
delete this._lastX;
|
|
2321
|
+
delete this._lastY;
|
|
1353
2322
|
}
|
|
1354
2323
|
});
|
|
1355
|
-
L.Map.addInitHook("addHandler", "shiftKeyRotate", L.Map.ShiftKeyRotate);
|
|
1356
2324
|
L.Map.addInitHook(function() {
|
|
1357
|
-
if (this.
|
|
2325
|
+
if (this.options.shiftKeyRotate) {
|
|
2326
|
+
this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
|
|
2327
|
+
this.shiftKeyRotate.enable();
|
|
2328
|
+
} else if (this.options.pitchDrag) {
|
|
2329
|
+
this.shiftKeyRotate = new L.Map.ShiftKeyRotate(this);
|
|
2330
|
+
this.shiftKeyRotate.addPitchHooks();
|
|
2331
|
+
this.on("unload", this.shiftKeyRotate.removePitchHooks, this.shiftKeyRotate);
|
|
2332
|
+
}
|
|
2333
|
+
if (this.scrollWheelZoom.enabled() && this.options.shiftKeyRotate) {
|
|
1358
2334
|
this.scrollWheelZoom.disable();
|
|
1359
2335
|
this.scrollWheelZoom.enable();
|
|
1360
2336
|
}
|
|
@@ -1370,6 +2346,8 @@ var leafletRotate = (() => {
|
|
|
1370
2346
|
* for touch-capable web browsers.
|
|
1371
2347
|
*
|
|
1372
2348
|
* @type {(Boolean|String)}
|
|
2349
|
+
*
|
|
2350
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1373
2351
|
*/
|
|
1374
2352
|
touchZoom: L.Browser.touch,
|
|
1375
2353
|
/**
|
|
@@ -1380,15 +2358,27 @@ var leafletRotate = (() => {
|
|
|
1380
2358
|
* when pinch-zooming.
|
|
1381
2359
|
*
|
|
1382
2360
|
* @type {Boolean}
|
|
2361
|
+
*
|
|
2362
|
+
* @since leaflet-rotate (v0.1.0)
|
|
1383
2363
|
*/
|
|
1384
2364
|
bounceAtZoomLimits: false
|
|
1385
2365
|
});
|
|
1386
2366
|
L.Map.TouchZoom = L.Handler.extend({
|
|
2367
|
+
/**
|
|
2368
|
+
* Enable pinch zoom handling and mark the shared touch handler as active.
|
|
2369
|
+
*
|
|
2370
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2371
|
+
*/
|
|
1387
2372
|
addHooks() {
|
|
1388
2373
|
this._map._container.classList.add("leaflet-touch-zoom");
|
|
1389
2374
|
this._map.touchGestures.enable();
|
|
1390
2375
|
this._map.touchGestures.zoom = true;
|
|
1391
2376
|
},
|
|
2377
|
+
/**
|
|
2378
|
+
* Disable pinch zoom handling while preserving touch rotation state.
|
|
2379
|
+
*
|
|
2380
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2381
|
+
*/
|
|
1392
2382
|
removeHooks() {
|
|
1393
2383
|
this._map._container.classList.remove("leaflet-touch-zoom");
|
|
1394
2384
|
this._map.touchGestures.zoom = false;
|
|
@@ -1400,93 +2390,166 @@ var leafletRotate = (() => {
|
|
|
1400
2390
|
L.Control.Rotate = L.Control.extend({
|
|
1401
2391
|
options: {
|
|
1402
2392
|
position: "topleft",
|
|
1403
|
-
closeOnZeroBearing: true
|
|
2393
|
+
closeOnZeroBearing: true,
|
|
2394
|
+
visualizePitch: true
|
|
1404
2395
|
},
|
|
2396
|
+
/**
|
|
2397
|
+
* Create the control element and subscribe to map rotation and pitch.
|
|
2398
|
+
*
|
|
2399
|
+
* @param {L.Map} map
|
|
2400
|
+
*
|
|
2401
|
+
* @returns {HTMLElement} Control container.
|
|
2402
|
+
*
|
|
2403
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2404
|
+
*/
|
|
1405
2405
|
onAdd(map) {
|
|
1406
2406
|
const container = this._container = L.DomUtil.create("div", "leaflet-control-rotate leaflet-bar");
|
|
1407
2407
|
const arrow = this._arrow = L.DomUtil.create("span", "leaflet-control-rotate-arrow");
|
|
1408
2408
|
arrow.style.backgroundImage = `url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")`;
|
|
1409
|
-
arrow.style.cursor = "grab";
|
|
1410
2409
|
arrow.style.display = "block";
|
|
1411
2410
|
arrow.style.width = "100%";
|
|
1412
2411
|
arrow.style.height = "100%";
|
|
1413
2412
|
arrow.style.backgroundRepeat = "no-repeat";
|
|
1414
2413
|
arrow.style.backgroundPosition = "50%";
|
|
2414
|
+
arrow.style.pointerEvents = "none";
|
|
1415
2415
|
const link = this._link = L.DomUtil.create("a", "leaflet-control-rotate-toggle", container);
|
|
1416
2416
|
link.appendChild(arrow);
|
|
1417
2417
|
link.href = "#";
|
|
1418
|
-
link.title = "
|
|
1419
|
-
|
|
2418
|
+
link.title = this.options.visualizePitch ? "Reset bearing and pitch" : "Reset bearing to north";
|
|
2419
|
+
link.setAttribute("role", "button");
|
|
2420
|
+
link.setAttribute("aria-label", link.title);
|
|
2421
|
+
link.style.cursor = "grab";
|
|
2422
|
+
link.style.touchAction = "none";
|
|
2423
|
+
this._onPointerMoveBound = this._onPointerMove.bind(this);
|
|
2424
|
+
this._onPointerUpBound = this._onPointerUp.bind(this);
|
|
2425
|
+
L.DomEvent.on(link, "dblclick contextmenu", L.DomEvent.stop).on(link, "click", L.DomEvent.stop).on(link, "click", this._onClick, this).on(link, "click", this._refocusOnMap, this);
|
|
2426
|
+
link.addEventListener("pointerdown", this._onPointerDown.bind(this));
|
|
2427
|
+
map.on("rotate pitch", this._restyle, this);
|
|
2428
|
+
this._map = map;
|
|
1420
2429
|
this._restyle();
|
|
1421
|
-
map.on("rotate", this._restyle, this);
|
|
1422
|
-
this._follow = false;
|
|
1423
|
-
this._canFollow = false;
|
|
1424
|
-
if (this.options.closeOnZeroBearing && map.getBearing() === 0) {
|
|
1425
|
-
container.style.display = "none";
|
|
1426
|
-
}
|
|
1427
2430
|
return container;
|
|
1428
2431
|
},
|
|
2432
|
+
/**
|
|
2433
|
+
* Remove the map event listener when the control is detached.
|
|
2434
|
+
*
|
|
2435
|
+
* @param {L.Map} map
|
|
2436
|
+
*
|
|
2437
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2438
|
+
*/
|
|
1429
2439
|
onRemove(map) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
this.dragging = false;
|
|
1442
|
-
L.DomEvent.off(document, "mousemove", this._handleMouseDrag, this).off(document, "mouseup", this._handleMouseUp, this);
|
|
1443
|
-
},
|
|
1444
|
-
_handleMouseDrag(e) {
|
|
1445
|
-
if (!this.dragging) {
|
|
2440
|
+
this._onPointerUp();
|
|
2441
|
+
map.off("rotate pitch", this._restyle, this);
|
|
2442
|
+
},
|
|
2443
|
+
/**
|
|
2444
|
+
* Reset bearing (and pitch) unless the click ends a drag.
|
|
2445
|
+
*
|
|
2446
|
+
* @since leaflet-rotate (0.5.1)
|
|
2447
|
+
*/
|
|
2448
|
+
_onClick() {
|
|
2449
|
+
if (this._moved) {
|
|
2450
|
+
this._moved = false;
|
|
1446
2451
|
return;
|
|
1447
2452
|
}
|
|
1448
|
-
|
|
1449
|
-
this._map.setBearing(deltaX);
|
|
1450
|
-
},
|
|
1451
|
-
_cycleState(ev) {
|
|
1452
|
-
if (!this._map) {
|
|
2453
|
+
if (!this._map || !this._map._rotate) {
|
|
1453
2454
|
return;
|
|
1454
2455
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
map.touchRotate.enable();
|
|
1458
|
-
} else if (!map.compassBearing.enabled()) {
|
|
1459
|
-
map.touchRotate.disable();
|
|
1460
|
-
(DeviceOrientationEvent && DeviceOrientationEvent.requestPermission ? DeviceOrientationEvent.requestPermission() : Promise.resolve("granted")).then((state) => "granted" === state && map.compassBearing.enable());
|
|
2456
|
+
if (this.options.visualizePitch) {
|
|
2457
|
+
this._map.resetNorthPitch();
|
|
1461
2458
|
} else {
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
2459
|
+
this._map.resetNorth();
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
/**
|
|
2463
|
+
* Start dragging the control (mouse, pen or touch).
|
|
2464
|
+
*
|
|
2465
|
+
* @param {PointerEvent} e
|
|
2466
|
+
*
|
|
2467
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2468
|
+
*/
|
|
2469
|
+
_onPointerDown(e) {
|
|
2470
|
+
if (!this._map || !this._map._rotate || e.button > 0 || this._pointerId != null) {
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
e.preventDefault();
|
|
2474
|
+
e.stopPropagation();
|
|
2475
|
+
this._pointerId = e.pointerId;
|
|
2476
|
+
this._startX = this._lastX = e.clientX;
|
|
2477
|
+
this._startY = this._lastY = e.clientY;
|
|
2478
|
+
this._moved = false;
|
|
2479
|
+
this._link.style.cursor = "grabbing";
|
|
2480
|
+
document.addEventListener("pointermove", this._onPointerMoveBound);
|
|
2481
|
+
document.addEventListener("pointerup", this._onPointerUpBound);
|
|
2482
|
+
document.addEventListener("pointercancel", this._onPointerUpBound);
|
|
2483
|
+
},
|
|
2484
|
+
/**
|
|
2485
|
+
* Horizontal movement changes the bearing, vertical movement the pitch.
|
|
2486
|
+
*
|
|
2487
|
+
* @param {PointerEvent} e
|
|
2488
|
+
*
|
|
2489
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2490
|
+
*/
|
|
2491
|
+
_onPointerMove(e) {
|
|
2492
|
+
if (e.pointerId !== this._pointerId) {
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
e.preventDefault();
|
|
2496
|
+
if (!this._moved && Math.hypot(e.clientX - this._startX, e.clientY - this._startY) < 3) {
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
if (!this._moved) {
|
|
2500
|
+
this._moved = true;
|
|
2501
|
+
this._map._stopPitchBearingAnimation();
|
|
2502
|
+
}
|
|
2503
|
+
const map = this._map, pitch = this.options.visualizePitch ? map.getPitch() + (this._lastY - e.clientY) * 0.5 : map.getPitch();
|
|
2504
|
+
map._setPitchAndBearing(pitch, map.getBearing() + (e.clientX - this._lastX) * 0.8);
|
|
2505
|
+
this._lastX = e.clientX;
|
|
2506
|
+
this._lastY = e.clientY;
|
|
2507
|
+
},
|
|
2508
|
+
/**
|
|
2509
|
+
* Stop dragging and remove document listeners.
|
|
2510
|
+
*
|
|
2511
|
+
* @param {PointerEvent} [e]
|
|
2512
|
+
*
|
|
2513
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2514
|
+
*/
|
|
2515
|
+
_onPointerUp(e) {
|
|
2516
|
+
if (e && e.pointerId !== this._pointerId) {
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
document.removeEventListener("pointermove", this._onPointerMoveBound);
|
|
2520
|
+
document.removeEventListener("pointerup", this._onPointerUpBound);
|
|
2521
|
+
document.removeEventListener("pointercancel", this._onPointerUpBound);
|
|
2522
|
+
if (e && this._moved && !this._link.contains(e.target)) {
|
|
2523
|
+
this._moved = false;
|
|
2524
|
+
}
|
|
2525
|
+
delete this._pointerId;
|
|
2526
|
+
if (this._link) {
|
|
2527
|
+
this._link.style.cursor = "grab";
|
|
2528
|
+
this._restyle();
|
|
1467
2529
|
}
|
|
1468
|
-
this._restyle();
|
|
1469
2530
|
},
|
|
2531
|
+
/**
|
|
2532
|
+
* Update the icon (bearing and pitch) and the control visibility.
|
|
2533
|
+
*
|
|
2534
|
+
* @listens L.Map#rotate
|
|
2535
|
+
* @listens L.Map#pitch
|
|
2536
|
+
*
|
|
2537
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2538
|
+
*/
|
|
1470
2539
|
_restyle() {
|
|
1471
|
-
|
|
2540
|
+
const map = this._map;
|
|
2541
|
+
if (!map) {
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
if (!map.options.rotate) {
|
|
1472
2545
|
this._link.classList.add("leaflet-disabled");
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
if (map.compassBearing.enabled()) {
|
|
1481
|
-
this._link.style.backgroundColor = "orange";
|
|
1482
|
-
} else if (map.touchRotate.enabled()) {
|
|
1483
|
-
this._link.style.backgroundColor = null;
|
|
1484
|
-
} else {
|
|
1485
|
-
this._link.style.backgroundColor = "grey";
|
|
1486
|
-
if (0 === bearing && this.options.closeOnZeroBearing) {
|
|
1487
|
-
this._container.style.display = "none";
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const bearing = map.getBearing(), pitch = map.getPitch();
|
|
2549
|
+
this._arrow.style.transform = this.options.visualizePitch && pitch ? `scale(${1 / Math.sqrt(Math.cos(pitch * Math.PI / 180))}) rotateX(${pitch}deg) rotate(${bearing}deg)` : `rotate(${bearing}deg)`;
|
|
2550
|
+
if (this.options.closeOnZeroBearing) {
|
|
2551
|
+
const hidden = !bearing && !(this.options.visualizePitch && pitch) && this._pointerId == null;
|
|
2552
|
+
this._container.style.display = hidden ? "none" : "block";
|
|
1490
2553
|
}
|
|
1491
2554
|
}
|
|
1492
2555
|
});
|
|
@@ -1494,6 +2557,14 @@ var leafletRotate = (() => {
|
|
|
1494
2557
|
L.control.rotate = (options) => new L.Control.Rotate(options);
|
|
1495
2558
|
}
|
|
1496
2559
|
L.Map.mergeOptions({
|
|
2560
|
+
/**
|
|
2561
|
+
* Add the rotation control automatically. Set to `false` to disable,
|
|
2562
|
+
* or pass an options object to configure the control.
|
|
2563
|
+
*
|
|
2564
|
+
* @type {boolean|Object}
|
|
2565
|
+
*
|
|
2566
|
+
* @since leaflet-rotate (v0.1.0)
|
|
2567
|
+
*/
|
|
1497
2568
|
rotateControl: true
|
|
1498
2569
|
});
|
|
1499
2570
|
L.Map.addInitHook(function() {
|