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