leaflet-rotate 0.5.0 → 0.5.2
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 +606 -168
- 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 +369 -72
- 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 +35 -0
- package/test/index.html +559 -185
- package/test/steps/example.steps.js +56 -29
- package/test/steps/gestures.steps.js +55 -0
- package/test/steps/{bearing.steps.js → rotation.steps.js} +312 -15
package/src/map/Map.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extends Leaflet maps with bearing rotation and 3D pitch.
|
|
3
3
|
*
|
|
4
|
+
* Coordinate spaces used by the overrides below:
|
|
5
|
+
* - projected pixels: CRS coordinates at a given zoom;
|
|
6
|
+
* - layer points: projected pixels minus `_pixelOrigin`, on the flat ground plane;
|
|
7
|
+
* - map-pane points: layer points after bearing/pitch, before the outer pane translation;
|
|
8
|
+
* - container points: map-pane points plus `_getMapPanePos()`, in viewport pixels.
|
|
9
|
+
*
|
|
10
|
+
* Tiles and vectors share `rotatePane`; upright markers and popups use `norotatePane`
|
|
11
|
+
* and the same point conversions. Public angles are degrees; `_bearing` and `_pitch`
|
|
12
|
+
* are radians. With pitch enabled, CSS and coordinate conversions must use the same
|
|
13
|
+
* projective transform, including while the outer map pane is being dragged.
|
|
14
|
+
*
|
|
4
15
|
* @external L.Map
|
|
5
16
|
*
|
|
6
17
|
* @since leaflet-rotate (v0.1.0)
|
|
@@ -47,6 +58,15 @@ L.Map.mergeOptions({
|
|
|
47
58
|
*/
|
|
48
59
|
pitch: 0,
|
|
49
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @type {number} Maximum distance of the pitched map plane from the map center, in multiples
|
|
63
|
+
* of the map height. Tiles, vectors and `getBounds()` stop there (and always
|
|
64
|
+
* before the horizon); a `.leaflet-pitch-sky` element fills the area above.
|
|
65
|
+
*
|
|
66
|
+
* @since leaflet-rotate (0.5.1)
|
|
67
|
+
*/
|
|
68
|
+
pitchMaxDistance: 3,
|
|
69
|
+
|
|
50
70
|
});
|
|
51
71
|
|
|
52
72
|
L.Map.include({
|
|
@@ -86,6 +106,10 @@ L.Map.include({
|
|
|
86
106
|
mapProto.initialize.apply(this, arguments);
|
|
87
107
|
if (this.options.rotate) {
|
|
88
108
|
this.on('resize', this._updatePitchTransform, this);
|
|
109
|
+
this.on('movestart dragstart', this._startPitchPan, this);
|
|
110
|
+
this.on('move', this._updatePitchPan, this);
|
|
111
|
+
this.on('moveend', this._endPitchPan, this);
|
|
112
|
+
this.on('move', this._updatePitchSky, this);
|
|
89
113
|
this.setBearing(this.options.bearing);
|
|
90
114
|
this.setPitch(this.options.pitch);
|
|
91
115
|
}
|
|
@@ -232,7 +256,8 @@ L.Map.include({
|
|
|
232
256
|
/**
|
|
233
257
|
* Return the geographic bounds of the current viewport, accounting for rotation and pitch.
|
|
234
258
|
*
|
|
235
|
-
* The result
|
|
259
|
+
* The result encloses the four corners of the visible ground rectangle. With pitch,
|
|
260
|
+
* the rectangle stops at the configured far plane, excluding the sky.
|
|
236
261
|
*
|
|
237
262
|
* @returns {L.LatLngBounds} Geographic bounds visible in the map viewport.
|
|
238
263
|
*
|
|
@@ -253,15 +278,66 @@ L.Map.include({
|
|
|
253
278
|
|
|
254
279
|
// LatLngBounds' constructor automatically
|
|
255
280
|
// extends the bounds to fit the passed points
|
|
256
|
-
const
|
|
281
|
+
const [min, max] = this._getPitchedContainerBounds(0);
|
|
257
282
|
return new L.LatLngBounds([
|
|
258
|
-
this.containerPointToLatLng([
|
|
259
|
-
this.containerPointToLatLng([
|
|
260
|
-
this.containerPointToLatLng([
|
|
261
|
-
this.containerPointToLatLng([
|
|
283
|
+
this.containerPointToLatLng([min.x, min.y]), // topleft
|
|
284
|
+
this.containerPointToLatLng([max.x, min.y]), // topright
|
|
285
|
+
this.containerPointToLatLng([max.x, max.y]), // bottomright
|
|
286
|
+
this.containerPointToLatLng([min.x, max.y]), // bottomleft
|
|
262
287
|
]);
|
|
263
288
|
},
|
|
264
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Return the container rectangle covering the visible map plane, optionally padded by a
|
|
292
|
+
* fraction of the map size. When pitched, its top is cut at the far plane (see `pitchMaxDistance`)
|
|
293
|
+
* so that it never reaches the horizon, where the plane would extend to infinity.
|
|
294
|
+
*
|
|
295
|
+
* @param {number} padding Padding as a fraction of the map size.
|
|
296
|
+
*
|
|
297
|
+
* @returns {L.Point[]} Top-left and bottom-right container points.
|
|
298
|
+
*
|
|
299
|
+
* @since leaflet-rotate (0.5.1)
|
|
300
|
+
*/
|
|
301
|
+
_getPitchedContainerBounds(padding) {
|
|
302
|
+
const size = this.getSize(),
|
|
303
|
+
min = size.multiplyBy(-padding),
|
|
304
|
+
max = size.multiplyBy(1 + padding);
|
|
305
|
+
if (this._pitch) {
|
|
306
|
+
min.y = Math.min(Math.max(min.y, this._getPitchFarY()), max.y);
|
|
307
|
+
}
|
|
308
|
+
return [min, max];
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Return the container y coordinate of the far plane: points above it are farther than
|
|
313
|
+
* `pitchMaxDistance` from the map center, or beyond the horizon.
|
|
314
|
+
*
|
|
315
|
+
* @returns {number} Container y coordinate (`-Infinity` when the whole map is close enough).
|
|
316
|
+
*
|
|
317
|
+
* @since leaflet-rotate (0.5.1)
|
|
318
|
+
*/
|
|
319
|
+
_getPitchFarY() {
|
|
320
|
+
const size = this.getSize(),
|
|
321
|
+
inverse = this._pitchInverseTransform,
|
|
322
|
+
panePos = this._getMapPanePos(),
|
|
323
|
+
x = size.x / 2 - panePos.x,
|
|
324
|
+
centerY = size.y / 2 - panePos.y,
|
|
325
|
+
maxDistance = this.options.pitchMaxDistance * size.y,
|
|
326
|
+
w = y => inverse.g * x + inverse.h * y + inverse.i,
|
|
327
|
+
centerW = w(centerY),
|
|
328
|
+
center = this._transformPitchPoint([x, centerY], inverse),
|
|
329
|
+
// the horizon is horizontal on screen (bearing is applied before the pitch)
|
|
330
|
+
isNear = y => w(y) * centerW > 0 && this._transformPitchPoint([x, y], inverse).distanceTo(center) <= maxDistance;
|
|
331
|
+
|
|
332
|
+
let far = -size.y - panePos.y, near = centerY;
|
|
333
|
+
if (isNear(far)) { return -Infinity; }
|
|
334
|
+
for (let i = 0; i < 20; i++) {
|
|
335
|
+
const mid = (far + near) / 2;
|
|
336
|
+
if (isNear(mid)) { near = mid; } else { far = mid; }
|
|
337
|
+
}
|
|
338
|
+
return near + panePos.y;
|
|
339
|
+
},
|
|
340
|
+
|
|
265
341
|
/**
|
|
266
342
|
* Returns the bounds of the current map view in projected pixel
|
|
267
343
|
* coordinates (sometimes useful in layer and overlay implementations).
|
|
@@ -298,48 +374,110 @@ L.Map.include({
|
|
|
298
374
|
if (!this._rotate) { return; }
|
|
299
375
|
|
|
300
376
|
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));
|
|
377
|
+
this._animatePitchBearing(this.getPitch(), theta, options);
|
|
333
378
|
return;
|
|
334
379
|
}
|
|
335
380
|
|
|
381
|
+
this._stopPitchBearingAnimation();
|
|
382
|
+
this._setBearing(theta);
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Animate pitch and bearing together (bearing follows the shortest path).
|
|
387
|
+
*
|
|
388
|
+
* @param {number} pitch Target pitch in degrees.
|
|
389
|
+
* @param {number} bearing Target bearing in degrees.
|
|
390
|
+
* @param {Object} [options] Animation options.
|
|
391
|
+
* @param {number} [options.duration=0.25] Animation duration in seconds.
|
|
392
|
+
* @param {number} [options.easeLinearity=0.5] Easing curve linearity.
|
|
393
|
+
*
|
|
394
|
+
* @since leaflet-rotate (0.5.1)
|
|
395
|
+
*/
|
|
396
|
+
_animatePitchBearing(pitch, bearing, options) {
|
|
397
|
+
this._stopPitchBearingAnimation();
|
|
398
|
+
const animationId = this._bearingAnimationId;
|
|
399
|
+
|
|
400
|
+
const startBearing = this.getBearing(),
|
|
401
|
+
startPitch = this.getPitch(),
|
|
402
|
+
endBearing = L.Util.wrapNum(bearing, [0, 360]),
|
|
403
|
+
endPitch = Math.max(0, Math.min(pitch, 85)),
|
|
404
|
+
delta = ((endBearing - startBearing + 540) % 360) - 180,
|
|
405
|
+
duration = Math.max(0, options.duration == null ? 0.25 : options.duration) * 1000,
|
|
406
|
+
easeLinearity = Math.max(options.easeLinearity || 0.5, 0.2),
|
|
407
|
+
startTime = performance.now();
|
|
408
|
+
|
|
409
|
+
const animate = function() {
|
|
410
|
+
if (!this._mapPane || this._bearingAnimationId !== animationId) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const progress = duration ? Math.min((performance.now() - startTime) / duration, 1) : 1,
|
|
415
|
+
eased = 1 - Math.pow(1 - progress, 1 / easeLinearity);
|
|
416
|
+
|
|
417
|
+
if (progress < 1) {
|
|
418
|
+
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
419
|
+
} else {
|
|
420
|
+
delete this._bearingAnimationFrame;
|
|
421
|
+
}
|
|
422
|
+
this._setPitchAndBearing(
|
|
423
|
+
progress === 1 ? endPitch : startPitch + (endPitch - startPitch) * eased,
|
|
424
|
+
progress === 1 ? endBearing : startBearing + delta * eased
|
|
425
|
+
);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
this._bearingAnimationFrame = requestAnimationFrame(animate.bind(this));
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Cancel a running pitch/bearing animation.
|
|
433
|
+
*
|
|
434
|
+
* @since leaflet-rotate (0.5.1)
|
|
435
|
+
*/
|
|
436
|
+
_stopPitchBearingAnimation() {
|
|
336
437
|
this._bearingAnimationId = (this._bearingAnimationId || 0) + 1;
|
|
337
438
|
if (this._bearingAnimationFrame) {
|
|
338
439
|
cancelAnimationFrame(this._bearingAnimationFrame);
|
|
339
440
|
delete this._bearingAnimationFrame;
|
|
340
441
|
}
|
|
442
|
+
},
|
|
341
443
|
|
|
342
|
-
|
|
444
|
+
/**
|
|
445
|
+
* Rotate the map back to north (bearing 0), animated by default.
|
|
446
|
+
*
|
|
447
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
448
|
+
*
|
|
449
|
+
* @returns {L.Map} This map instance.
|
|
450
|
+
*
|
|
451
|
+
* @since leaflet-rotate (0.5.1)
|
|
452
|
+
*/
|
|
453
|
+
resetNorth(options) {
|
|
454
|
+
options = Object.assign({ animate: true, duration: 0.5 }, options);
|
|
455
|
+
if (options.animate) {
|
|
456
|
+
this._animatePitchBearing(this.getPitch(), 0, options);
|
|
457
|
+
} else {
|
|
458
|
+
this.setBearing(0);
|
|
459
|
+
}
|
|
460
|
+
return this;
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Rotate the map back to north and remove the pitch, animated by default.
|
|
465
|
+
*
|
|
466
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
467
|
+
*
|
|
468
|
+
* @returns {L.Map} This map instance.
|
|
469
|
+
*
|
|
470
|
+
* @since leaflet-rotate (0.5.1)
|
|
471
|
+
*/
|
|
472
|
+
resetNorthPitch(options) {
|
|
473
|
+
options = Object.assign({ animate: true, duration: 0.5 }, options);
|
|
474
|
+
if (options.animate) {
|
|
475
|
+
this._animatePitchBearing(0, 0, options);
|
|
476
|
+
} else {
|
|
477
|
+
this._stopPitchBearingAnimation();
|
|
478
|
+
this._setPitchAndBearing(0, 0);
|
|
479
|
+
}
|
|
480
|
+
return this;
|
|
343
481
|
},
|
|
344
482
|
|
|
345
483
|
/**
|
|
@@ -384,10 +522,12 @@ L.Map.include({
|
|
|
384
522
|
},
|
|
385
523
|
|
|
386
524
|
/**
|
|
387
|
-
* Transform a point with a
|
|
525
|
+
* Transform a ground-plane point with a DOMMatrix, or reverse its screen projection.
|
|
526
|
+
* Both branches divide by the homogeneous coordinate after applying the matrix.
|
|
388
527
|
*
|
|
389
528
|
* @param {L.Point|number[]} point Input pixel coordinate.
|
|
390
|
-
* @param {DOMMatrix|Object} matrix Forward or inverse
|
|
529
|
+
* @param {DOMMatrix|Object} matrix Forward CSS matrix or inverse homography with
|
|
530
|
+
* row-major coefficients `a` through `i`.
|
|
391
531
|
*
|
|
392
532
|
* @returns {L.Point} Transformed pixel coordinate.
|
|
393
533
|
*
|
|
@@ -420,9 +560,25 @@ L.Map.include({
|
|
|
420
560
|
position = this._rotatePane._leaflet_pos || new L.Point(0, 0),
|
|
421
561
|
transform = `translate3d(${pivot.x}px,${pivot.y}px,0) perspective(${perspective}px) rotateX(${this.getPitch()}deg) rotateZ(${this.getBearing()}deg) translate3d(${position.x - pivot.x}px,${position.y - pivot.y}px,0)`;
|
|
422
562
|
|
|
563
|
+
this._setPitchTransform(transform);
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Apply the CSS transform and cache matching forward/inverse point conversions.
|
|
568
|
+
*
|
|
569
|
+
* Ground points have z = 0, so the 4x4 CSS matrix reduces to the homography
|
|
570
|
+
* [a b c; d e f; g h i]. Its inverse maps screen points back onto that plane;
|
|
571
|
+
* inverting the full 4x4 matrix at screen z = 0 would not undo perspective.
|
|
572
|
+
*
|
|
573
|
+
* @param {string} transform CSS transform list, including a serialized DOMMatrix.
|
|
574
|
+
*
|
|
575
|
+
* @since leaflet-rotate (0.5.2)
|
|
576
|
+
*/
|
|
577
|
+
_setPitchTransform(transform) {
|
|
423
578
|
this._rotatePane.style.transformOrigin = '0 0';
|
|
424
579
|
this._rotatePane.style.transform = transform;
|
|
425
|
-
|
|
580
|
+
// parse the transform directly: getComputedStyle() would force a style recalculation on every frame
|
|
581
|
+
const matrix = this._pitchTransform = new DOMMatrix(transform),
|
|
426
582
|
a = matrix.m11, b = matrix.m21, c = matrix.m41,
|
|
427
583
|
d = matrix.m12, e = matrix.m22, f = matrix.m42,
|
|
428
584
|
g = matrix.m14, h = matrix.m24, i = matrix.m44,
|
|
@@ -439,12 +595,108 @@ L.Map.include({
|
|
|
439
595
|
h: (b * g - a * h) / determinant,
|
|
440
596
|
i: (a * e - b * d) / determinant,
|
|
441
597
|
};
|
|
598
|
+
this._updatePitchSky();
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Capture a fixed projection baseline for a pitched drag or programmatic pan.
|
|
603
|
+
*
|
|
604
|
+
* `movestart` uses the viewport center; `dragstart` replaces it with the original
|
|
605
|
+
* pointer position. Store both the screen anchor and its ground-plane coordinate
|
|
606
|
+
* so subsequent frames can keep the grabbed geographic point under the pointer.
|
|
607
|
+
* Pane positions are in screen pixels and the baseline matrices stay unchanged.
|
|
608
|
+
*
|
|
609
|
+
* @param {L.Event} e Leaflet `movestart` or `dragstart` event.
|
|
610
|
+
*
|
|
611
|
+
* @since leaflet-rotate (0.5.2)
|
|
612
|
+
*/
|
|
613
|
+
_startPitchPan(e) {
|
|
614
|
+
if (!this._pitch || !this._loaded) { return; }
|
|
615
|
+
const draggable = this.dragging && this.dragging._draggable,
|
|
616
|
+
anchor = e.type === 'dragstart' && draggable
|
|
617
|
+
? (this.pointerEventToContainerPoint || this.mouseEventToContainerPoint).call(this, {
|
|
618
|
+
clientX: draggable._startPoint.x, clientY: draggable._startPoint.y,
|
|
619
|
+
})
|
|
620
|
+
: this.getSize().divideBy(2),
|
|
621
|
+
panePos = this._getMapPanePos().clone();
|
|
622
|
+
this._pitchPan = {
|
|
623
|
+
anchor,
|
|
624
|
+
panePos,
|
|
625
|
+
lastPanePos: panePos,
|
|
626
|
+
matrix: this._pitchTransform,
|
|
627
|
+
inverse: this._pitchInverseTransform,
|
|
628
|
+
layerAnchor: this.containerPointToLayerPoint(anchor),
|
|
629
|
+
zoom: this._zoom,
|
|
630
|
+
};
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Translate the ground plane without moving the camera's vanishing point.
|
|
635
|
+
*
|
|
636
|
+
* For pane displacement delta, invert the displaced anchor with the baseline
|
|
637
|
+
* projection to find the required layer-space shift. Compose
|
|
638
|
+
* T(-delta) * baseline * T(shift): the left translation cancels Leaflet's outer
|
|
639
|
+
* pane movement, while the right translation moves geography before perspective.
|
|
640
|
+
* Always derive this from the baseline, not the previous frame, to avoid drift.
|
|
641
|
+
* Zoom frames are handled by Leaflet's zoom/origin updates instead.
|
|
642
|
+
*
|
|
643
|
+
* @since leaflet-rotate (0.5.2)
|
|
644
|
+
*/
|
|
645
|
+
_updatePitchPan() {
|
|
646
|
+
const pan = this._pitchPan;
|
|
647
|
+
if (!this._pitch || !pan || this._animatingZoom || this._zoom !== pan.zoom) { return; }
|
|
648
|
+
const panePos = this._getMapPanePos();
|
|
649
|
+
if (panePos.equals(pan.lastPanePos)) { return; }
|
|
650
|
+
const delta = panePos.subtract(pan.panePos),
|
|
651
|
+
target = this._transformPitchPoint(pan.anchor.add(delta).subtract(pan.panePos), pan.inverse),
|
|
652
|
+
shift = target.subtract(pan.layerAnchor);
|
|
653
|
+
const matrix = new DOMMatrix().translate(-delta.x, -delta.y)
|
|
654
|
+
.multiply(pan.matrix).translate(shift.x, shift.y);
|
|
655
|
+
this._setPitchTransform(matrix.toString());
|
|
656
|
+
pan.lastPanePos = panePos.clone();
|
|
657
|
+
delete this._lastCenter;
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Discard the baseline only at `moveend`, after any drag inertia has completed.
|
|
662
|
+
* Keep the final transform and pixel origin: resetting the view here would
|
|
663
|
+
* reproject layers abruptly and cause a visible jump on release.
|
|
664
|
+
*
|
|
665
|
+
* @since leaflet-rotate (0.5.2)
|
|
666
|
+
*/
|
|
667
|
+
_endPitchPan() {
|
|
668
|
+
delete this._pitchPan;
|
|
669
|
+
},
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Fill the area above the far plane (where no tiles are loaded) with a sky gradient.
|
|
673
|
+
* Style it through the `.leaflet-pitch-sky` class.
|
|
674
|
+
*
|
|
675
|
+
* @since leaflet-rotate (0.5.1)
|
|
676
|
+
*/
|
|
677
|
+
_updatePitchSky() {
|
|
678
|
+
const farY = this._pitch && this._pitchInverseTransform ? this._getPitchFarY() : -Infinity;
|
|
679
|
+
if (farY <= 0) {
|
|
680
|
+
if (this._pitchSky) { this._pitchSky.style.display = 'none'; }
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (!this._pitchSky) {
|
|
684
|
+
this._pitchSky = L.DomUtil.create('div', 'leaflet-pitch-sky', this._container);
|
|
685
|
+
Object.assign(this._pitchSky.style, {
|
|
686
|
+
position: 'absolute', left: 0, top: 0, width: '100%', zIndex: 0, pointerEvents: 'none',
|
|
687
|
+
background: 'linear-gradient(#9ec5e8, #dfe9f0)',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
this._pitchSky.style.display = '';
|
|
691
|
+
// overlap the far edge slightly to hide antialiasing gaps
|
|
692
|
+
this._pitchSky.style.height = `${Math.ceil(farY) + 1}px`;
|
|
442
693
|
},
|
|
443
694
|
|
|
444
695
|
/**
|
|
445
696
|
* Set the map pitch in degrees. Values are clamped to the range 0–85.
|
|
446
697
|
*
|
|
447
698
|
* @param {number} pitch Pitch angle in degrees; clamped to [0, 85].
|
|
699
|
+
* @param {Object} [options] Animation options (`animate`, `duration`, `easeLinearity`).
|
|
448
700
|
*
|
|
449
701
|
* @returns {L.Map} This map instance.
|
|
450
702
|
*
|
|
@@ -452,7 +704,12 @@ L.Map.include({
|
|
|
452
704
|
*
|
|
453
705
|
* @since leaflet-rotate (0.5.0)
|
|
454
706
|
*/
|
|
455
|
-
setPitch(pitch) {
|
|
707
|
+
setPitch(pitch, options) {
|
|
708
|
+
if (options && options.animate) {
|
|
709
|
+
this._animatePitchBearing(pitch, this.getBearing(), options);
|
|
710
|
+
return this;
|
|
711
|
+
}
|
|
712
|
+
this._stopPitchBearingAnimation();
|
|
456
713
|
this._setPitchAndBearing(pitch, this.getBearing());
|
|
457
714
|
return this;
|
|
458
715
|
},
|
|
@@ -484,6 +741,7 @@ L.Map.include({
|
|
|
484
741
|
}
|
|
485
742
|
this._setBearing(bearing, mapCenter);
|
|
486
743
|
if (pitchChanged) {
|
|
744
|
+
if (!this._pitch) { this._updatePitchSky(); }
|
|
487
745
|
this.fire('pitch');
|
|
488
746
|
}
|
|
489
747
|
},
|
|
@@ -499,6 +757,38 @@ L.Map.include({
|
|
|
499
757
|
return (this._pitch || 0) * (180 / Math.PI);
|
|
500
758
|
},
|
|
501
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Zoom while keeping the given point fixed on screen, also when the map is pitched.
|
|
762
|
+
*
|
|
763
|
+
* Leaflet interpolates in container space, which is not linear under perspective;
|
|
764
|
+
* interpolating in layer space keeps the point under the cursor.
|
|
765
|
+
*
|
|
766
|
+
* @param {L.LatLng|L.Point} latlng Geographic point or container point to keep fixed.
|
|
767
|
+
* @param {number} zoom Target zoom level.
|
|
768
|
+
* @param {Object} [options] Zoom options.
|
|
769
|
+
*
|
|
770
|
+
* @returns {L.Map} This map instance.
|
|
771
|
+
*
|
|
772
|
+
* @since leaflet-rotate (0.5.1)
|
|
773
|
+
*/
|
|
774
|
+
setZoomAround(latlng, zoom, options) {
|
|
775
|
+
if (!this._pitch) {
|
|
776
|
+
return mapProto.setZoomAround.apply(this, arguments);
|
|
777
|
+
}
|
|
778
|
+
const scale = this.getZoomScale(zoom),
|
|
779
|
+
size = this.getSize(),
|
|
780
|
+
center = this.containerPointToLayerPoint(size.divideBy(2)),
|
|
781
|
+
containerPoint = latlng instanceof L.Point ? latlng : this.latLngToContainerPoint(latlng),
|
|
782
|
+
point = this.containerPointToLayerPoint(containerPoint),
|
|
783
|
+
newCenter = this.layerPointToLatLng(center.add(point.subtract(center).multiplyBy(1 - 1 / scale)));
|
|
784
|
+
options = Object.assign({}, options);
|
|
785
|
+
// Leaflet skips the animation when the origin is "far" in layer space, which is always the case near the horizon.
|
|
786
|
+
if (options.animate === undefined && new L.Bounds(new L.Point(0, 0), size).contains(containerPoint)) {
|
|
787
|
+
options.animate = true;
|
|
788
|
+
}
|
|
789
|
+
return this.setView(newCenter, zoom, { zoom: options });
|
|
790
|
+
},
|
|
791
|
+
|
|
502
792
|
/**
|
|
503
793
|
* Get the current map bearing. Enable the `rotate` map option before calling.
|
|
504
794
|
*
|
|
@@ -778,24 +1068,28 @@ L.Map.include({
|
|
|
778
1068
|
// },
|
|
779
1069
|
|
|
780
1070
|
/**
|
|
781
|
-
* Return the
|
|
1071
|
+
* Return the screen-space pan offset needed to center the given geographic point.
|
|
1072
|
+
* Under perspective, projecting a negated layer offset is not equivalent to
|
|
1073
|
+
* negating its screen projection. Reflect the target about the layer center
|
|
1074
|
+
* to match the opposite anchor displacement performed by Leaflet's `panBy`.
|
|
782
1075
|
*
|
|
783
1076
|
* @param {L.LatLng} latlng Geographic point.
|
|
784
1077
|
*
|
|
785
|
-
* @returns {L.Point} Offset in
|
|
1078
|
+
* @returns {L.Point} Offset in container pixels (the map pane is panned in screen space), adjusted for bearing and pitch.
|
|
786
1079
|
*
|
|
787
1080
|
* @since leaflet-rotate (v0.1.0)
|
|
788
1081
|
*/
|
|
789
1082
|
_getCenterOffset(latlng) {
|
|
790
|
-
|
|
1083
|
+
if (!this._rotate) {
|
|
1084
|
+
return mapProto._getCenterOffset.apply(this, arguments);
|
|
1085
|
+
}
|
|
791
1086
|
if (this._pitch) {
|
|
792
|
-
const center = this.getSize().
|
|
793
|
-
|
|
794
|
-
.subtract(this.
|
|
795
|
-
|
|
796
|
-
centerOffset = centerOffset.rotate(this._bearing);
|
|
1087
|
+
const center = this.getSize().divideBy(2),
|
|
1088
|
+
layerCenter = this.containerPointToLayerPoint(center),
|
|
1089
|
+
opposite = layerCenter.multiplyBy(2).subtract(this.project(latlng).subtract(this._pixelOrigin));
|
|
1090
|
+
return center.subtract(this.layerPointToContainerPoint(opposite));
|
|
797
1091
|
}
|
|
798
|
-
return
|
|
1092
|
+
return this.latLngToContainerPoint(latlng)._subtract(this.getSize()._divideBy(2));
|
|
799
1093
|
},
|
|
800
1094
|
|
|
801
1095
|
/**
|
|
@@ -826,16 +1120,13 @@ L.Map.include({
|
|
|
826
1120
|
* @since leaflet-rotate (v0.1.0)
|
|
827
1121
|
*/
|
|
828
1122
|
_getNewPixelOrigin(center, zoom) {
|
|
829
|
-
if (!this._rotate
|
|
1123
|
+
if (!this._rotate) {
|
|
830
1124
|
return mapProto._getNewPixelOrigin.apply(this, arguments);
|
|
831
1125
|
}
|
|
832
|
-
|
|
1126
|
+
// the layer point under the view center doesn't depend on zoom (pane transforms only rotate/pitch around the pivot),
|
|
1127
|
+
// so this matches `_setBearing` also when the pivot is no longer the view center (eg. after panning)
|
|
833
1128
|
return this.project(center, zoom)
|
|
834
|
-
.
|
|
835
|
-
._subtract(viewHalf)
|
|
836
|
-
._add(this._getMapPanePos())
|
|
837
|
-
._add(this._getRotatePanePos())
|
|
838
|
-
.rotate(-this._bearing)
|
|
1129
|
+
._subtract(this.containerPointToLayerPoint(this.getSize()._divideBy(2)))
|
|
839
1130
|
._round();
|
|
840
1131
|
},
|
|
841
1132
|
|
|
@@ -860,21 +1151,28 @@ L.Map.include({
|
|
|
860
1151
|
const mapZoom = this._animatingZoom ? Math.max(this._animateToZoom, this.getZoom()) : this.getZoom(),
|
|
861
1152
|
scale = this.getZoomScale(mapZoom, zoom),
|
|
862
1153
|
pixelCenter = this.project(center, zoom).floor(),
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1154
|
+
[min, max] = this._getPitchedContainerBounds(0),
|
|
1155
|
+
corners = [[min.x, min.y], [max.x, min.y], [min.x, max.y], [max.x, max.y]]
|
|
1156
|
+
.map(point => this.containerPointToLayerPoint(point));
|
|
1157
|
+
|
|
1158
|
+
if (this._pitch) {
|
|
1159
|
+
// the pitched view is not symmetric: the far side covers much more than the near one
|
|
1160
|
+
const viewCenter = this.containerPointToLayerPoint(this.getSize()._divideBy(2));
|
|
1161
|
+
return new L.Bounds(corners.map(point => pixelCenter.add(point.subtract(viewCenter).divideBy(scale))));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const halfSize = new L.Bounds(corners.map(point => point.floor())).getSize().divideBy(scale * 2);
|
|
870
1165
|
|
|
871
1166
|
return new L.Bounds(pixelCenter.subtract(halfSize), pixelCenter.add(halfSize));
|
|
872
1167
|
},
|
|
873
1168
|
|
|
874
1169
|
/**
|
|
1170
|
+
* Return the viewport center before the outer map-pane translation.
|
|
1171
|
+
* Used as the bearing/pitch pivot in map-pane coordinates, not layer coordinates.
|
|
1172
|
+
*
|
|
875
1173
|
* @since leaflet-rotate (v0.2.0)
|
|
876
1174
|
*
|
|
877
|
-
* @returns {L.Point} Map pivot
|
|
1175
|
+
* @returns {L.Point} Map pivot in map-pane pixels.
|
|
878
1176
|
*/
|
|
879
1177
|
_getPixelCenter() {
|
|
880
1178
|
if (!this._rotate && mapProto._getPixelCenter) {
|
|
@@ -884,6 +1182,9 @@ L.Map.include({
|
|
|
884
1182
|
},
|
|
885
1183
|
|
|
886
1184
|
/**
|
|
1185
|
+
* Convert the padded visible ground rectangle to layer bounds for vector renderers.
|
|
1186
|
+
* Clip its far edge before reversing perspective to avoid unbounded render areas.
|
|
1187
|
+
*
|
|
887
1188
|
* @see src\layer\vector\Renderer::_update()
|
|
888
1189
|
*
|
|
889
1190
|
* @param {number} padding Bounds padding as a fraction of map size.
|
|
@@ -896,11 +1197,7 @@ L.Map.include({
|
|
|
896
1197
|
if (!this._rotate && mapProto._getPaddedPixelBounds) {
|
|
897
1198
|
return mapProto._getPaddedPixelBounds.apply(this, arguments);
|
|
898
1199
|
}
|
|
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();
|
|
1200
|
+
const [padMin, padMax] = this._getPitchedContainerBounds(padding);
|
|
904
1201
|
|
|
905
1202
|
return new L.Bounds([
|
|
906
1203
|
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.
|