leaflet-rotate 0.5.2 → 0.5.4
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 +2 -2
- package/build/server.js +19 -3
- package/dist/leaflet-rotate-src.js +229 -75
- 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 +3 -0
- package/lib/debug.min.js +1 -1
- package/package.json +1 -1
- package/src/dom/Draggable.js +20 -8
- package/src/index.js +1 -0
- package/src/layer/marker/Marker.js +24 -11
- package/src/layer/tile/GridLayer.js +16 -1
- package/src/map/Map.js +154 -60
- package/src/map/handler/Drag.js +59 -0
- package/test/features/example.feature +5 -0
- package/test/features/harness.feature +84 -0
- package/test/features/navigation-bounds.feature +33 -0
- package/test/features/overlays.feature +25 -0
- package/test/features/pitch.feature +19 -0
- package/test/index.html +90 -11
- package/test/steps/example.steps.js +18 -0
- package/test/steps/harness.steps.js +207 -0
- package/test/steps/navigation-bounds.steps.js +195 -0
- package/test/steps/overlays.steps.js +186 -0
- package/test/steps/pitch.steps.js +264 -0
- package/test/steps/rotation.steps.js +28 -4
- package/test/support/hooks.js +20 -8
package/lib/debug.js
CHANGED
package/lib/debug.min.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(){const e=Object.isExtensible(globalThis.L)?globalThis.L:Object.assign({},globalThis.L);globalThis.L=e,e.Rotate={debug(t,o={}){this._map=t,document.head.insertAdjacentHTML("beforeend",'<style>.leaflet-rotate-tools{display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;margin-bottom:6px}.leaflet-rotate-tools input,.leaflet-rotate-tools button{vertical-align:middle}.leaflet-rotate-info{border-collapse:collapse}.leaflet-rotate-info td,.leaflet-rotate-info th{padding:1px 12px 1px 0;text-align:left;white-space:nowrap;vertical-align:top}.leaflet-rotate-info td:nth-of-type(1),.leaflet-rotate-info td:nth-of-type(2){font-family:monospace;min-width:12ch}.leaflet-rotate-info tr[title]{cursor:help}div.crosshair{position:absolute;width:1px;height:1px;border:1px solid green;z-index:100;left:50%;right:50%}div.pivot,div.pixelorigin{position:absolute;width:1px;height:1px;border:5px solid yellow;border-radius:100%;z-index:1000}div.pivot:before{left:-1.5em;bottom:-2em;position:absolute;content:"PIVOT";color:#ff0}div.pixelorigin:before{left:-1.75em;top:-2em;position:absolute;content:"ORIGIN";color:#ff0}div.panebounds.main{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid red;border-bottom:1px red dashed;border-right:1px red dashed;z-index:100}div.panebounds.rotate{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid purple;border-bottom:1px purple dashed;border-right:1px purple dashed;z-index:100}div.crosshair.c1{left:calc(50% - 5px);top:50%;height:0;width:10px}div.crosshair.c2{left:50%;top:calc(50% - 5px);width:0;height:10px}#rho_input{vertical-align:middle}.panebounds:before{padding:1ch}.panebounds.rotate:before{float:right;content:"ROTATED"}.panebounds.main:before{float:left;content:"MAIN"}</style>'),t._container.insertAdjacentHTML("afterbegin",'<div class="crosshair c1"></div><div class="crosshair c2"></div>');const i=`
|
|
1
|
+
(function(){const e=Object.isExtensible(globalThis.L)?globalThis.L:Object.assign({},globalThis.L);globalThis.L=e,e.Rotate={debug(t,o={}){this._map=t,document.head.insertAdjacentHTML("beforeend",'<style>.leaflet-rotate-tools{display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;margin-bottom:6px}.leaflet-rotate-tools input,.leaflet-rotate-tools button{vertical-align:middle}.leaflet-rotate-info{border-collapse:collapse}.leaflet-rotate-info td,.leaflet-rotate-info th{padding:1px 12px 1px 0;text-align:left;white-space:nowrap;vertical-align:top}.leaflet-rotate-info td:nth-of-type(1),.leaflet-rotate-info td:nth-of-type(2){font-family:monospace;min-width:12ch}.leaflet-rotate-info tr[title]{cursor:help}div.crosshair,div.pivot,div.pixelorigin,div.panebounds{pointer-events:none}div.crosshair{position:absolute;width:1px;height:1px;border:1px solid green;z-index:100;left:50%;right:50%}div.pivot,div.pixelorigin{position:absolute;width:1px;height:1px;border:5px solid yellow;border-radius:100%;z-index:1000}div.pivot:before{left:-1.5em;bottom:-2em;position:absolute;content:"PIVOT";color:#ff0}div.pixelorigin:before{left:-1.75em;top:-2em;position:absolute;content:"ORIGIN";color:#ff0}div.panebounds.main{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid red;border-bottom:1px red dashed;border-right:1px red dashed;z-index:100}div.panebounds.rotate{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid purple;border-bottom:1px purple dashed;border-right:1px purple dashed;z-index:100}div.crosshair.c1{left:calc(50% - 5px);top:50%;height:0;width:10px}div.crosshair.c2{left:50%;top:calc(50% - 5px);width:0;height:10px}#rho_input{vertical-align:middle}.panebounds:before{padding:1ch}.panebounds.rotate:before{float:right;content:"ROTATED"}.panebounds.main:before{float:left;content:"MAIN"}</style>'),t._container.insertAdjacentHTML("afterbegin",'<div class="crosshair c1"></div><div class="crosshair c2"></div>');const i=`
|
|
2
2
|
<div class="leaflet-rotate-tools">
|
|
3
3
|
<span>Bearing
|
|
4
4
|
${[0,15,30,45,60,75,90,180].map(n=>`<button onclick="L.Rotate._map.setBearing(${n});" title="map.setBearing(${n});">${n}</button>`).join("")}
|
package/package.json
CHANGED
package/src/dom/Draggable.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Preserve the final position when an active drag returns within its starting
|
|
3
|
+
* click tolerance. Marker geographic coordinates are adapted in `L.Marker`.
|
|
4
4
|
*
|
|
5
5
|
* @external L.Draggable
|
|
6
6
|
*
|
|
@@ -15,11 +15,23 @@
|
|
|
15
15
|
* @since leaflet-rotate (v0.1.0)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
L.Draggable.
|
|
19
|
-
|
|
20
|
-
/** @TODO */
|
|
21
|
-
// updateMapBearing(mapBearing) {
|
|
22
|
-
// this._mapBearing = mapBearing;
|
|
23
|
-
// },
|
|
18
|
+
const draggableProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Draggable.prototype));
|
|
24
19
|
|
|
20
|
+
L.Draggable.include({
|
|
21
|
+
_onMove(e) {
|
|
22
|
+
const pointer = e.touches ? e.touches[0] : e;
|
|
23
|
+
if (this._enabled && this._moving && (!e.touches || e.touches.length === 1)) {
|
|
24
|
+
const offset = new L.Point(pointer.clientX, pointer.clientY).subtract(this._startPoint);
|
|
25
|
+
if ((!offset.x && !offset.y) || Math.abs(offset.x) + Math.abs(offset.y) < this.options.clickTolerance) {
|
|
26
|
+
L.DomEvent.preventDefault(e);
|
|
27
|
+
this._newPos = this._startPos.add(new L.Point(
|
|
28
|
+
offset.x / this._parentScale.x, offset.y / this._parentScale.y
|
|
29
|
+
));
|
|
30
|
+
this._lastEvent = e;
|
|
31
|
+
this._updatePosition();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return draggableProto._onMove.apply(this, arguments);
|
|
36
|
+
},
|
|
25
37
|
});
|
package/src/index.js
CHANGED
|
@@ -53,6 +53,7 @@ export * from './layer/vector/Renderer';
|
|
|
53
53
|
export * from './map/Map';
|
|
54
54
|
export * from './map/handler/CompassBearing';
|
|
55
55
|
export * from './map/handler/ContainerMutation';
|
|
56
|
+
export * from './map/handler/Drag';
|
|
56
57
|
export * from './map/handler/TouchGestures';
|
|
57
58
|
export * from './map/handler/TouchRotate';
|
|
58
59
|
export * from './map/handler/TouchPitch';
|
|
@@ -201,17 +201,7 @@ L.Marker.include({
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/** @TODO use markerProto._setPos */
|
|
204
|
-
|
|
205
|
-
if (this.options.rotateWithView) {
|
|
206
|
-
bearing += this._map._bearing;
|
|
207
|
-
}
|
|
208
|
-
if (this.options.keepUpright) {
|
|
209
|
-
bearing = ((bearing % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
|
|
210
|
-
if (bearing > Math.PI / 2 && bearing < 3 * Math.PI / 2) {
|
|
211
|
-
bearing += Math.PI;
|
|
212
|
-
}
|
|
213
|
-
bearing %= 2 * Math.PI;
|
|
214
|
-
}
|
|
204
|
+
const bearing = this._getIconBearing();
|
|
215
205
|
|
|
216
206
|
/** @TODO use markerProto._setPos */
|
|
217
207
|
if (this._icon) {
|
|
@@ -228,6 +218,29 @@ L.Marker.include({
|
|
|
228
218
|
this._resetZIndex();
|
|
229
219
|
},
|
|
230
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Compute the on-screen icon rotation from `rotation`, `rotateWithView` and `keepUpright`.
|
|
223
|
+
*
|
|
224
|
+
* @returns {number} Angle in radians.
|
|
225
|
+
*
|
|
226
|
+
* @since leaflet-rotate (0.5.3)
|
|
227
|
+
*/
|
|
228
|
+
_getIconBearing() {
|
|
229
|
+
let bearing = this.options.rotation || 0;
|
|
230
|
+
if (this.options.rotateWithView) {
|
|
231
|
+
bearing += this._map._bearing;
|
|
232
|
+
}
|
|
233
|
+
if (this.options.keepUpright) {
|
|
234
|
+
bearing = ((bearing % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
|
|
235
|
+
if (bearing > Math.PI / 2 && bearing < 3 * Math.PI / 2) {
|
|
236
|
+
bearing += Math.PI;
|
|
237
|
+
}
|
|
238
|
+
bearing %= 2 * Math.PI;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return bearing;
|
|
242
|
+
},
|
|
243
|
+
|
|
231
244
|
// _updateZIndex(offset) {
|
|
232
245
|
// if (!this._map._rotate) {
|
|
233
246
|
// return markerProto._updateZIndex.apply(this, arguments);
|
|
@@ -32,9 +32,24 @@ L.GridLayer.include({
|
|
|
32
32
|
return events;
|
|
33
33
|
},
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Preserve the 3D context on tile containers without making individual tile levels 3D.
|
|
37
|
+
*
|
|
38
|
+
* @since leaflet-rotate (0.5.3)
|
|
39
|
+
*/
|
|
40
|
+
_initContainer() {
|
|
41
|
+
gridLayerProto._initContainer.apply(this, arguments);
|
|
42
|
+
if (!this._map._rotate || !this._container) { return; }
|
|
43
|
+
// Tile levels stay flat: preserving 3D on them also clips images in Firefox.
|
|
44
|
+
for (let pane = this._container; pane && pane !== this._map._rotatePane; pane = pane.parentElement) {
|
|
45
|
+
pane.style.transformStyle = 'preserve-3d';
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
35
49
|
/**
|
|
36
50
|
* Keep tile levels aligned when a bearing/pitch change shifts the pixel origin
|
|
37
51
|
* (eg. when the rotation pivot is not the view center), then refresh tiles (throttled).
|
|
52
|
+
* The first event also synchronizes levels created before any accumulated pans.
|
|
38
53
|
*
|
|
39
54
|
* @listens L.Map~rotate
|
|
40
55
|
*
|
|
@@ -44,7 +59,7 @@ L.GridLayer.include({
|
|
|
44
59
|
const map = this._map;
|
|
45
60
|
if (!map) { return; }
|
|
46
61
|
const origin = map.getPixelOrigin();
|
|
47
|
-
if (this._rotateOrigin
|
|
62
|
+
if ((!this._rotateOrigin || !this._rotateOrigin.equals(origin)) && !map._animatingZoom && this._levels) {
|
|
48
63
|
this._setZoomTransforms(map.getCenter(), map.getZoom());
|
|
49
64
|
}
|
|
50
65
|
this._rotateOrigin = origin;
|
package/src/map/Map.js
CHANGED
|
@@ -71,6 +71,20 @@ L.Map.mergeOptions({
|
|
|
71
71
|
|
|
72
72
|
L.Map.include({
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Rebuild perspective before Leaflet calculates the origin of a reset view.
|
|
76
|
+
* A reset clears the outer pane translation, unlike a completed pan.
|
|
77
|
+
*/
|
|
78
|
+
_resetView() {
|
|
79
|
+
if (this._pitch) {
|
|
80
|
+
delete this._pitchPan;
|
|
81
|
+
L.DomUtil.setPosition(this._mapPane, new L.Point(0, 0));
|
|
82
|
+
this._updatePitchTransform();
|
|
83
|
+
arguments[0] = this._limitCenter(arguments[0], arguments[1], this.options.maxBounds);
|
|
84
|
+
}
|
|
85
|
+
return mapProto._resetView.apply(this, arguments);
|
|
86
|
+
},
|
|
87
|
+
|
|
74
88
|
/**
|
|
75
89
|
* Apply an immediate animated-zoom frame without waiting for a transition delay.
|
|
76
90
|
*
|
|
@@ -110,6 +124,7 @@ L.Map.include({
|
|
|
110
124
|
this.on('move', this._updatePitchPan, this);
|
|
111
125
|
this.on('moveend', this._endPitchPan, this);
|
|
112
126
|
this.on('move', this._updatePitchSky, this);
|
|
127
|
+
this.on('rotate', this._panInsideMaxBounds, this);
|
|
113
128
|
this.setBearing(this.options.bearing);
|
|
114
129
|
this.setPitch(this.options.pitch);
|
|
115
130
|
}
|
|
@@ -528,22 +543,34 @@ L.Map.include({
|
|
|
528
543
|
* @param {L.Point|number[]} point Input pixel coordinate.
|
|
529
544
|
* @param {DOMMatrix|Object} matrix Forward CSS matrix or inverse homography with
|
|
530
545
|
* row-major coefficients `a` through `i`.
|
|
546
|
+
* @param {L.Point} [anchor] Keep inverse pan coordinates on this anchor's side of the horizon.
|
|
531
547
|
*
|
|
532
548
|
* @returns {L.Point} Transformed pixel coordinate.
|
|
533
549
|
*
|
|
534
550
|
* @since leaflet-rotate (0.5.0)
|
|
535
551
|
*/
|
|
536
|
-
_transformPitchPoint(point, matrix) {
|
|
552
|
+
_transformPitchPoint(point, matrix, anchor) {
|
|
537
553
|
point = toPoint(point);
|
|
538
554
|
if (!(matrix instanceof DOMMatrix)) {
|
|
555
|
+
if (anchor) {
|
|
556
|
+
const anchorW = matrix.g * anchor.x + matrix.h * anchor.y + matrix.i,
|
|
557
|
+
pointW = matrix.g * point.x + matrix.h * point.y + matrix.i;
|
|
558
|
+
// Keep a constrained pan on the near side of the horizon.
|
|
559
|
+
if (pointW * anchorW <= 0) {
|
|
560
|
+
point = anchor.add(point.subtract(anchor).multiplyBy(anchorW / (anchorW - pointW) * (1 - 1e-6)));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
539
563
|
const denominator = matrix.g * point.x + matrix.h * point.y + matrix.i;
|
|
540
564
|
return new L.Point(
|
|
541
565
|
(matrix.a * point.x + matrix.b * point.y + matrix.c) / denominator,
|
|
542
566
|
(matrix.d * point.x + matrix.e * point.y + matrix.f) / denominator
|
|
543
567
|
);
|
|
544
568
|
}
|
|
545
|
-
const
|
|
546
|
-
return new L.Point(
|
|
569
|
+
const denominator = matrix.m14 * point.x + matrix.m24 * point.y + matrix.m44;
|
|
570
|
+
return new L.Point(
|
|
571
|
+
(matrix.m11 * point.x + matrix.m21 * point.y + matrix.m41) / denominator,
|
|
572
|
+
(matrix.m12 * point.x + matrix.m22 * point.y + matrix.m42) / denominator
|
|
573
|
+
);
|
|
547
574
|
},
|
|
548
575
|
|
|
549
576
|
/**
|
|
@@ -576,7 +603,6 @@ L.Map.include({
|
|
|
576
603
|
*/
|
|
577
604
|
_setPitchTransform(transform) {
|
|
578
605
|
this._rotatePane.style.transformOrigin = '0 0';
|
|
579
|
-
this._rotatePane.style.transform = transform;
|
|
580
606
|
// parse the transform directly: getComputedStyle() would force a style recalculation on every frame
|
|
581
607
|
const matrix = this._pitchTransform = new DOMMatrix(transform),
|
|
582
608
|
a = matrix.m11, b = matrix.m21, c = matrix.m41,
|
|
@@ -584,6 +610,11 @@ L.Map.include({
|
|
|
584
610
|
g = matrix.m14, h = matrix.m24, i = matrix.m44,
|
|
585
611
|
determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
|
586
612
|
|
|
613
|
+
// Preserve invertibility for Firefox and scale homogeneous coefficients to avoid CSS rounding.
|
|
614
|
+
const ground = DOMMatrix.fromMatrix(matrix);
|
|
615
|
+
ground.m13 = ground.m23 = ground.m43 = 0;
|
|
616
|
+
this._rotatePane.style.transform = `matrix3d(${Array.from(ground.toFloat64Array(), value => value * 1000).join(',')})`;
|
|
617
|
+
|
|
587
618
|
this._pitchInverseTransform = {
|
|
588
619
|
a: (e * i - f * h) / determinant,
|
|
589
620
|
b: (c * h - b * i) / determinant,
|
|
@@ -670,7 +701,7 @@ L.Map.include({
|
|
|
670
701
|
|
|
671
702
|
/**
|
|
672
703
|
* Fill the area above the far plane (where no tiles are loaded) with a sky gradient.
|
|
673
|
-
* Style it through
|
|
704
|
+
* Style it through `.leaflet-pitch-sky` or the `--leaflet-sky-background` CSS property.
|
|
674
705
|
*
|
|
675
706
|
* @since leaflet-rotate (0.5.1)
|
|
676
707
|
*/
|
|
@@ -684,7 +715,7 @@ L.Map.include({
|
|
|
684
715
|
this._pitchSky = L.DomUtil.create('div', 'leaflet-pitch-sky', this._container);
|
|
685
716
|
Object.assign(this._pitchSky.style, {
|
|
686
717
|
position: 'absolute', left: 0, top: 0, width: '100%', zIndex: 0, pointerEvents: 'none',
|
|
687
|
-
background: 'linear-gradient(#9ec5e8, #dfe9f0)',
|
|
718
|
+
background: 'var(--leaflet-sky-background, linear-gradient(#9ec5e8, #dfe9f0))',
|
|
688
719
|
});
|
|
689
720
|
}
|
|
690
721
|
this._pitchSky.style.display = '';
|
|
@@ -856,6 +887,8 @@ L.Map.include({
|
|
|
856
887
|
|
|
857
888
|
if (this._rotate) {
|
|
858
889
|
this._rotatePane = this.createPane('rotatePane', this._mapPane);
|
|
890
|
+
// Keep the context stable at pitch zero too: Firefox clips tiles when it is removed.
|
|
891
|
+
this._mapPane.style.transformStyle = this._rotatePane.style.transformStyle = 'preserve-3d';
|
|
859
892
|
this._norotatePane = this.createPane('norotatePane', this._mapPane);
|
|
860
893
|
if (L.version.startsWith('2.')) {
|
|
861
894
|
this._rotatePane.style.transformOrigin = '0 0';
|
|
@@ -899,10 +932,11 @@ L.Map.include({
|
|
|
899
932
|
* @since leaflet-rotate (v0.1.4)
|
|
900
933
|
*/
|
|
901
934
|
panInside(latlng, options) {
|
|
902
|
-
if (!this._rotate
|
|
935
|
+
if (!this._rotate) {
|
|
903
936
|
return mapProto.panInside.apply(this, arguments);
|
|
904
937
|
}
|
|
905
938
|
|
|
939
|
+
// The rotate pane can retain an offset after returning to bearing zero.
|
|
906
940
|
options = options || {};
|
|
907
941
|
|
|
908
942
|
const paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
|
|
@@ -934,54 +968,111 @@ L.Map.include({
|
|
|
934
968
|
},
|
|
935
969
|
|
|
936
970
|
/**
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
971
|
+
* Constrain the visible ground footprint, including its asymmetric pitched far side.
|
|
972
|
+
* When it is larger than maxBounds, Leaflet's rebound centers the excess on each axis.
|
|
973
|
+
*/
|
|
974
|
+
_limitCenter(center, zoom, bounds) {
|
|
975
|
+
if (!this._rotate || !bounds) {
|
|
976
|
+
return mapProto._limitCenter.apply(this, arguments);
|
|
977
|
+
}
|
|
978
|
+
const centerPoint = this.project(center, zoom),
|
|
979
|
+
layerCenter = this.containerPointToLayerPoint(this.getSize().divideBy(2)),
|
|
980
|
+
footprint = this._getPaddedPixelBounds(0, false),
|
|
981
|
+
viewBounds = new L.Bounds(
|
|
982
|
+
centerPoint.add(footprint.min.subtract(layerCenter)),
|
|
983
|
+
centerPoint.add(footprint.max.subtract(layerCenter))
|
|
984
|
+
),
|
|
985
|
+
offset = this._getBoundsOffset(viewBounds, bounds, zoom);
|
|
986
|
+
if (Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) { return center; }
|
|
987
|
+
return this.unproject(centerPoint.add(offset), zoom);
|
|
988
|
+
},
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Reapply configured maximum bounds after a map transform.
|
|
943
992
|
*
|
|
944
|
-
* @
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
993
|
+
* @since leaflet-rotate (0.5.3)
|
|
994
|
+
*/
|
|
995
|
+
_panInsideMaxBounds() {
|
|
996
|
+
if (this.options.maxBounds) {
|
|
997
|
+
return mapProto._panInsideMaxBounds.apply(this, arguments);
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Keep the visible ground footprint inside the given bounds.
|
|
1003
|
+
*
|
|
1004
|
+
* @param {L.LatLngBounds|Array} bounds Geographic bounds.
|
|
1005
|
+
* @param {Object} [options] Pan options passed to Leaflet.
|
|
949
1006
|
* @returns {L.Map} This map instance.
|
|
950
|
-
*
|
|
951
|
-
* @since leaflet-rotate (
|
|
1007
|
+
*
|
|
1008
|
+
* @since leaflet-rotate (0.5.3)
|
|
952
1009
|
*/
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1010
|
+
panInsideBounds(bounds, options) {
|
|
1011
|
+
if (this._rotate) {
|
|
1012
|
+
bounds = bounds instanceof L.LatLngBounds ? bounds : new L.LatLngBounds(bounds);
|
|
1013
|
+
const center = this.getCenter(),
|
|
1014
|
+
limited = this._limitCenter(center, this._zoom, bounds);
|
|
1015
|
+
const offset = this._getCenterOffset(limited);
|
|
1016
|
+
// Screen-space rounding cannot reliably apply subpixel pans at high pitch.
|
|
1017
|
+
if (!center.equals(limited) && Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) {
|
|
1018
|
+
this._enforcingBounds = true;
|
|
1019
|
+
this.setView(limited, this._zoom, { reset: true });
|
|
1020
|
+
this._enforcingBounds = false;
|
|
1021
|
+
return this;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return mapProto.panInsideBounds.apply(this, arguments);
|
|
1025
|
+
},
|
|
965
1026
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1027
|
+
/**
|
|
1028
|
+
* Leaflet supplies inertia offsets in screen pixels, not projected CRS pixels.
|
|
1029
|
+
* Keep the drag's moving anchor when inertia continues a pitched pan.
|
|
1030
|
+
*/
|
|
1031
|
+
_limitOffset(offset, bounds) {
|
|
1032
|
+
if (!this._rotate || !bounds) {
|
|
1033
|
+
return mapProto._limitOffset.apply(this, arguments);
|
|
1034
|
+
}
|
|
1035
|
+
const pan = this._pitchPan,
|
|
1036
|
+
anchor = pan ? pan.anchor.add(this._getMapPanePos().subtract(pan.panePos)) : this.getSize().divideBy(2),
|
|
1037
|
+
layerAnchor = this.containerPointToLayerPoint(anchor),
|
|
1038
|
+
panePos = this._getMapPanePos(),
|
|
1039
|
+
target = anchor.subtract(offset).subtract(panePos),
|
|
1040
|
+
shift = (this._pitch
|
|
1041
|
+
? this._transformPitchPoint(target, this._pitchInverseTransform, anchor.subtract(panePos))
|
|
1042
|
+
: this.containerPointToLayerPoint(anchor.subtract(offset))).subtract(layerAnchor),
|
|
1043
|
+
footprint = this._getPaddedPixelBounds(0, false),
|
|
1044
|
+
origin = this.getPixelOrigin().subtract(shift),
|
|
1045
|
+
correction = this._getBoundsOffset(new L.Bounds(
|
|
1046
|
+
footprint.min.add(origin), footprint.max.add(origin)
|
|
1047
|
+
), bounds);
|
|
1048
|
+
return anchor.subtract(this.layerPointToContainerPoint(layerAnchor.add(shift).subtract(correction)));
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Pan by a screen-pixel offset, constraining large immediate pans to maxBounds.
|
|
1053
|
+
*
|
|
1054
|
+
* @param {L.Point|number[]} offset Pan offset in pixels.
|
|
1055
|
+
* @param {Object} [options] Pan options passed to Leaflet.
|
|
1056
|
+
* @returns {L.Map} This map instance.
|
|
1057
|
+
*
|
|
1058
|
+
* @since leaflet-rotate (0.5.3)
|
|
1059
|
+
*/
|
|
1060
|
+
panBy(offset, options) {
|
|
1061
|
+
offset = toPoint(offset);
|
|
1062
|
+
if (this._rotate && this._loaded && !this.getSize().contains(offset) && !(options && options.animate)) {
|
|
1063
|
+
const anchor = this.getSize().divideBy(2),
|
|
1064
|
+
panePos = this._getMapPanePos(),
|
|
1065
|
+
layerAnchor = this.containerPointToLayerPoint(anchor),
|
|
1066
|
+
target = anchor.subtract(offset).subtract(panePos),
|
|
1067
|
+
shift = (this._pitch
|
|
1068
|
+
? this._transformPitchPoint(target, this._pitchInverseTransform, anchor.subtract(panePos))
|
|
1069
|
+
: this.containerPointToLayerPoint(anchor.subtract(offset))).subtract(layerAnchor),
|
|
1070
|
+
center = this.unproject(this.project(this.getCenter()).subtract(shift));
|
|
1071
|
+
this._resetView(this._limitCenter(center, this._zoom, this.options.maxBounds), this._zoom);
|
|
1072
|
+
return this;
|
|
1073
|
+
}
|
|
1074
|
+
return mapProto.panBy.call(this, offset, options);
|
|
1075
|
+
},
|
|
985
1076
|
|
|
986
1077
|
// @method flyToBounds(bounds: LatLngBounds, options?: fitBounds options): this
|
|
987
1078
|
// Sets the view of the map with a smooth animation like [`flyTo`](#map-flyto),
|
|
@@ -1089,7 +1180,8 @@ L.Map.include({
|
|
|
1089
1180
|
opposite = layerCenter.multiplyBy(2).subtract(this.project(latlng).subtract(this._pixelOrigin));
|
|
1090
1181
|
return center.subtract(this.layerPointToContainerPoint(opposite));
|
|
1091
1182
|
}
|
|
1092
|
-
return this.
|
|
1183
|
+
return this.layerPointToContainerPoint(this.project(latlng).subtract(this._pixelOrigin))
|
|
1184
|
+
._subtract(this.getSize()._divideBy(2));
|
|
1093
1185
|
},
|
|
1094
1186
|
|
|
1095
1187
|
/**
|
|
@@ -1125,9 +1217,9 @@ L.Map.include({
|
|
|
1125
1217
|
}
|
|
1126
1218
|
// the layer point under the view center doesn't depend on zoom (pane transforms only rotate/pitch around the pivot),
|
|
1127
1219
|
// so this matches `_setBearing` also when the pivot is no longer the view center (eg. after panning)
|
|
1220
|
+
// Keep subpixel centers exact: wheel bursts magnify rounding from earlier zooms.
|
|
1128
1221
|
return this.project(center, zoom)
|
|
1129
|
-
._subtract(this.containerPointToLayerPoint(this.getSize()._divideBy(2)))
|
|
1130
|
-
._round();
|
|
1222
|
+
._subtract(this.containerPointToLayerPoint(this.getSize()._divideBy(2)));
|
|
1131
1223
|
},
|
|
1132
1224
|
|
|
1133
1225
|
/**
|
|
@@ -1188,23 +1280,25 @@ L.Map.include({
|
|
|
1188
1280
|
* @see src\layer\vector\Renderer::_update()
|
|
1189
1281
|
*
|
|
1190
1282
|
* @param {number} padding Bounds padding as a fraction of map size.
|
|
1283
|
+
* @param {boolean} [round=true] Round renderer bounds; navigation needs exact ground coordinates.
|
|
1191
1284
|
*
|
|
1192
1285
|
* @returns {L.Bounds} Padded bounds in layer-point coordinates.
|
|
1193
1286
|
*
|
|
1194
1287
|
* @since leaflet-rotate (v0.2.0)
|
|
1195
1288
|
*/
|
|
1196
|
-
_getPaddedPixelBounds(padding) {
|
|
1289
|
+
_getPaddedPixelBounds(padding, round = true) {
|
|
1197
1290
|
if (!this._rotate && mapProto._getPaddedPixelBounds) {
|
|
1198
1291
|
return mapProto._getPaddedPixelBounds.apply(this, arguments);
|
|
1199
1292
|
}
|
|
1200
1293
|
const [padMin, padMax] = this._getPitchedContainerBounds(padding);
|
|
1201
1294
|
|
|
1202
1295
|
return new L.Bounds([
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
this.containerPointToLayerPoint(
|
|
1207
|
-
|
|
1296
|
+
[padMin.x, padMin.y], [padMin.x, padMax.y],
|
|
1297
|
+
[padMax.x, padMin.y], [padMax.x, padMax.y]
|
|
1298
|
+
].map(point => {
|
|
1299
|
+
const projected = this.containerPointToLayerPoint(point);
|
|
1300
|
+
return round ? projected.floor() : projected;
|
|
1301
|
+
}));
|
|
1208
1302
|
},
|
|
1209
1303
|
|
|
1210
1304
|
/**
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply maxBoundsViscosity in projected ground coordinates before moving the map pane.
|
|
3
|
+
*/
|
|
4
|
+
const dragProto = Object.defineProperties({}, Object.getOwnPropertyDescriptors(L.Map.Drag.prototype));
|
|
5
|
+
|
|
6
|
+
L.Map.Drag.include({
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Capture the ground footprint and pointer anchor before a constrained drag.
|
|
10
|
+
*/
|
|
11
|
+
_onDragStart() {
|
|
12
|
+
dragProto._onDragStart.apply(this, arguments);
|
|
13
|
+
const map = this._map;
|
|
14
|
+
this._rotateLimit = null;
|
|
15
|
+
if (!map._rotate || !map.options.maxBounds || !map.options.maxBoundsViscosity) { return; }
|
|
16
|
+
const anchor = (map.pointerEventToContainerPoint || map.mouseEventToContainerPoint).call(map, {
|
|
17
|
+
clientX: this._draggable._startPoint.x, clientY: this._draggable._startPoint.y,
|
|
18
|
+
}),
|
|
19
|
+
footprint = map._getPaddedPixelBounds(0, false),
|
|
20
|
+
origin = map.getPixelOrigin();
|
|
21
|
+
this._rotateLimit = {
|
|
22
|
+
anchor,
|
|
23
|
+
layerAnchor: map.containerPointToLayerPoint(anchor),
|
|
24
|
+
panePos: map._getMapPanePos().clone(),
|
|
25
|
+
footprint: new L.Bounds(footprint.min.add(origin), footprint.max.add(origin)),
|
|
26
|
+
matrix: map._pitch ? map._pitchTransform : null,
|
|
27
|
+
inverse: map._pitchInverseTransform,
|
|
28
|
+
bearing: map._bearing,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Apply max-bounds viscosity in ground coordinates during a rotated drag.
|
|
34
|
+
*/
|
|
35
|
+
_onPreDragLimit() {
|
|
36
|
+
const limit = this._rotateLimit;
|
|
37
|
+
if (!this._map._rotate || !limit) {
|
|
38
|
+
return dragProto._onPreDragLimit.apply(this, arguments);
|
|
39
|
+
}
|
|
40
|
+
const map = this._map,
|
|
41
|
+
draggable = this._draggable,
|
|
42
|
+
offset = draggable._newPos.subtract(draggable._startPos),
|
|
43
|
+
shift = limit.matrix
|
|
44
|
+
? map._transformPitchPoint(limit.anchor.add(offset).subtract(limit.panePos), limit.inverse,
|
|
45
|
+
limit.anchor.subtract(limit.panePos))
|
|
46
|
+
.subtract(limit.layerAnchor)
|
|
47
|
+
: offset.rotate(-limit.bearing),
|
|
48
|
+
correction = map._getBoundsOffset(new L.Bounds(
|
|
49
|
+
limit.footprint.min.subtract(shift), limit.footprint.max.subtract(shift)
|
|
50
|
+
), map.options.maxBounds);
|
|
51
|
+
shift._subtract(correction.multiplyBy(this._viscosity));
|
|
52
|
+
const limitedOffset = limit.matrix
|
|
53
|
+
? map._transformPitchPoint(limit.layerAnchor.add(shift), limit.matrix)
|
|
54
|
+
.add(limit.panePos).subtract(limit.anchor)
|
|
55
|
+
: shift.rotate(limit.bearing);
|
|
56
|
+
draggable._newPos = draggable._startPos.add(limitedOffset);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
});
|
|
@@ -51,6 +51,11 @@ Feature: Rotating a Leaflet map
|
|
|
51
51
|
When I inspect debugger geometry after resizing and moving the map
|
|
52
52
|
Then the debugger reports the current map geometry
|
|
53
53
|
|
|
54
|
+
@debugger @issue-76
|
|
55
|
+
Scenario: Debugger overlays do not intercept marker clicks
|
|
56
|
+
When I place the debugger overlays over a marker
|
|
57
|
+
Then the marker receives the pointer events
|
|
58
|
+
|
|
54
59
|
@example-ui
|
|
55
60
|
Scenario: Example base-layer radios select one layer
|
|
56
61
|
When I switch between the example base layers
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Feature: Interactive regression scenarios have executable BDD coverage
|
|
2
|
+
Background:
|
|
3
|
+
Given the interactive regression page is open
|
|
4
|
+
|
|
5
|
+
Scenario: Every interactive scenario has a BDD example
|
|
6
|
+
Then every interactive scenario is listed in the BDD examples
|
|
7
|
+
|
|
8
|
+
Scenario: An active drag can finish within its starting click tolerance
|
|
9
|
+
Then active drags return to their starting position without turning clicks into drags
|
|
10
|
+
|
|
11
|
+
Scenario Outline: Camera gestures retain their intended behavior
|
|
12
|
+
When I run the interactive scenario "<scenario>"
|
|
13
|
+
Then the interactive camera result is "<result>"
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
| scenario | result |
|
|
17
|
+
| Ctrl+drag up: pitch from center | pitch up |
|
|
18
|
+
| Ctrl+drag down: pitch clamps at 0 | pitch zero |
|
|
19
|
+
| Ctrl+drag far up: pitch clamps at max | pitch max |
|
|
20
|
+
| Ctrl+drag right, just above center | clockwise |
|
|
21
|
+
| Ctrl+drag right, just below center | counterclockwise |
|
|
22
|
+
| Ctrl+drag right, far above center (angular) | clockwise |
|
|
23
|
+
| Ctrl+drag right, far below center (angular) | counterclockwise |
|
|
24
|
+
| Ctrl+drag diagonal, pitched + rotated | pitch and bearing |
|
|
25
|
+
| Ctrl+drag fast (2 big steps) | pitch and bearing |
|
|
26
|
+
| Ctrl+drag full circle around center | pitch and bearing |
|
|
27
|
+
| Ctrl+drag leaving the map | pitch and bearing |
|
|
28
|
+
| Ctrl+drag starting on a marker | pitch and bearing |
|
|
29
|
+
| Ctrl released mid-drag | pitch and bearing |
|
|
30
|
+
| Ctrl pressed mid-pan (should keep panning) | pan |
|
|
31
|
+
| Right-drag up + right | pitch and bearing |
|
|
32
|
+
| Ctrl+drag then plain pan | rotate then pan |
|
|
33
|
+
| Shift+wheel rotate | shift rotate |
|
|
34
|
+
| Wheel then Ctrl+drag while zoom pending | cancel zoom |
|
|
35
|
+
| Wheel during Ctrl+drag | zoom and rotate |
|
|
36
|
+
|
|
37
|
+
Scenario Outline: Wheel input zooms around its geographic anchor
|
|
38
|
+
When I run the interactive scenario "<scenario>"
|
|
39
|
+
Then the interactive camera result is "<result>"
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
| scenario | result |
|
|
43
|
+
| Wheel in at top-right, flat (control case) | zoom in |
|
|
44
|
+
| Wheel in at top-right, rotated | zoom in |
|
|
45
|
+
| Wheel in at top-right, pitch 60 | zoom in |
|
|
46
|
+
| Wheel out at bottom-left, pitch 45 | zoom out |
|
|
47
|
+
| Wheel near horizon, pitch 75 | zoom in |
|
|
48
|
+
| Ctrl+wheel pinch-in burst (trackpad), pitch 50 | zoom in |
|
|
49
|
+
| Ctrl+wheel pinch-out burst (trackpad), rotated | zoom out |
|
|
50
|
+
| Ctrl+wheel mouse notches at center | zoom in |
|
|
51
|
+
| Low zoom rotated, wheel in (layer jump check) | zoom in |
|
|
52
|
+
|
|
53
|
+
Scenario Outline: Pitched drags retain their geographic anchor
|
|
54
|
+
When I run the interactive scenario "<scenario>"
|
|
55
|
+
Then the interactive camera result is "pan"
|
|
56
|
+
|
|
57
|
+
Examples:
|
|
58
|
+
| scenario |
|
|
59
|
+
| Pan pitched 70 |
|
|
60
|
+
| Pan pitched 70: vertical up |
|
|
61
|
+
| Pan pitched 70: vertical down |
|
|
62
|
+
| Pan pitched 70: horizontal left |
|
|
63
|
+
| Pan pitched 70: horizontal right |
|
|
64
|
+
| Pan pitched 70: diagonal up-right |
|
|
65
|
+
| Pan pitched 70: diagonal down-left |
|
|
66
|
+
| Pan pitched 70: reverse vertical drag |
|
|
67
|
+
| Pan pitched 70: multi-direction loop |
|
|
68
|
+
| Pan pitched 70, bearing 225: diagonal |
|
|
69
|
+
|
|
70
|
+
Scenario Outline: Navigation reaches its destination while pitched and rotated
|
|
71
|
+
When I run the interactive scenario "<scenario>"
|
|
72
|
+
Then the interactive camera result is "navigate"
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
| scenario |
|
|
76
|
+
| Go to: Circle |
|
|
77
|
+
| Go to: Polygon |
|
|
78
|
+
| Go to: Path |
|
|
79
|
+
| Go to: Kyiv |
|
|
80
|
+
| Go to: London |
|
|
81
|
+
| Go to: San Francisco |
|
|
82
|
+
| Go to: Washington |
|
|
83
|
+
| Go to: Trondheim |
|
|
84
|
+
| Go to: Rectangle (fit from z 0) |
|