leaflet-rotate 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -5
- package/build/test.js +18 -0
- package/dist/leaflet-rotate-src.js +1081 -303
- package/dist/leaflet-rotate-src.js.map +3 -3
- package/dist/leaflet-rotate.js +1 -1
- package/dist/leaflet-rotate.js.map +3 -3
- package/lib/debug.js +84 -66
- package/lib/debug.min.js +1 -1
- package/misc/leaflet-heat-rotate.js +14 -7
- package/misc/loremIpsum.js +1 -1
- package/misc/places.js +1 -1
- package/misc/route.js +1 -1
- package/package.json +5 -5
- package/src/control/Rotate.js +98 -29
- package/src/dom/DomUtil.js +42 -47
- package/src/dom/Draggable.js +6 -1
- package/src/geo/LatLngBounds.js +5 -0
- package/src/geometry/Bounds.js +5 -0
- package/src/geometry/Point.js +33 -10
- package/src/index.js +3 -1
- package/src/layer/DivOverlay.js +43 -12
- package/src/layer/Popup.js +29 -16
- package/src/layer/Tooltip.js +21 -5
- package/src/layer/marker/Icon.js +20 -8
- package/src/layer/marker/Marker.js +103 -24
- package/src/layer/tile/GridLayer.js +22 -6
- package/src/layer/vector/Renderer.js +102 -11
- package/src/map/Map.js +408 -121
- package/src/map/handler/CompassBearing.js +33 -7
- package/src/map/handler/ContainerMutation.js +20 -3
- package/src/map/handler/ShiftKeyRotate.js +156 -16
- package/src/map/handler/TouchGestures.js +91 -29
- package/src/map/handler/TouchRotate.js +20 -3
- package/src/map/handler/TouchZoom.js +25 -5
- package/test/features/example.feature +5 -0
- package/test/features/rotation.feature +30 -0
- package/test/index.html +60 -22
- package/test/steps/bearing.steps.js +345 -3
- package/test/steps/example.steps.js +72 -11
- package/test/steps/gestures.steps.js +4 -4
- package/test/steps/overlays.steps.js +32 -32
- package/test/support/hooks.js +38 -2
|
@@ -21,6 +21,36 @@ Feature: Rotating a Leaflet map
|
|
|
21
21
|
When I animate the bearing to 90 degrees
|
|
22
22
|
Then the bearing reaches 90 degrees gradually
|
|
23
23
|
|
|
24
|
+
@issue-72
|
|
25
|
+
Scenario: Pitch projects and reverses map coordinates
|
|
26
|
+
When I set the map pitch to 45 degrees
|
|
27
|
+
Then map coordinates stay consistent with the pitched view
|
|
28
|
+
|
|
29
|
+
@issue-72
|
|
30
|
+
Scenario: Ctrl-drag changes pitch and bearing without panning
|
|
31
|
+
When I change pitch and bearing with Ctrl-drag
|
|
32
|
+
Then pitch and bearing change without moving the map pane
|
|
33
|
+
|
|
34
|
+
@issue-72
|
|
35
|
+
Scenario: Pitched vector layers stay aligned while panning
|
|
36
|
+
When I drag the pitched map
|
|
37
|
+
Then vector layers stay aligned with their map coordinates
|
|
38
|
+
|
|
39
|
+
@issue-72
|
|
40
|
+
Scenario: Vector layers stay aligned after off-center zoom and rotation
|
|
41
|
+
When I zoom off-center then rotate the map
|
|
42
|
+
Then vector layers stay aligned with their map coordinates
|
|
43
|
+
|
|
44
|
+
@issue-72
|
|
45
|
+
Scenario: Ctrl-drag after panning keeps the map center stable
|
|
46
|
+
When I pan the map then Ctrl-drag
|
|
47
|
+
Then the map center stays stable during Ctrl-drag
|
|
48
|
+
|
|
49
|
+
@issue-72
|
|
50
|
+
Scenario: Ctrl-drag cancels a pending wheel zoom
|
|
51
|
+
When I Ctrl-drag while a wheel zoom is pending
|
|
52
|
+
Then the pending wheel zoom does not move the map
|
|
53
|
+
|
|
24
54
|
@issue-25
|
|
25
55
|
Scenario: Fit bounds zoom is stable
|
|
26
56
|
When I fit the same bounds twice
|
package/test/index.html
CHANGED
|
@@ -6,18 +6,12 @@
|
|
|
6
6
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
7
7
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
<script src="https://unpkg.com/leaflet@1.9/dist/leaflet-src.js"></script>
|
|
11
|
-
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9/dist/leaflet.css">
|
|
12
|
-
|
|
13
|
-
<!-- Leaflet-Rotate -->
|
|
14
|
-
<script src="../dist/leaflet-rotate-src.js"></script>
|
|
9
|
+
<link id="leaflet-css" rel="stylesheet">
|
|
15
10
|
|
|
16
11
|
<!-- include some miscellaneous -->
|
|
17
12
|
<script src="../misc/route.js"></script>
|
|
18
13
|
<script src="../misc/places.js"></script>
|
|
19
14
|
<script src="../misc/loremIpsum.js"></script>
|
|
20
|
-
<script src="../lib/debug.js"></script>
|
|
21
15
|
|
|
22
16
|
<style>
|
|
23
17
|
#map {
|
|
@@ -36,6 +30,12 @@
|
|
|
36
30
|
|
|
37
31
|
<body>
|
|
38
32
|
|
|
33
|
+
<label for="leaflet-version">Leaflet version:</label>
|
|
34
|
+
<select id="leaflet-version">
|
|
35
|
+
<option value="1">1.9.4</option>
|
|
36
|
+
<option value="2">2.0.0-alpha.1</option>
|
|
37
|
+
</select>
|
|
38
|
+
|
|
39
39
|
<div id="map"></div>
|
|
40
40
|
|
|
41
41
|
<hr>
|
|
@@ -61,8 +61,38 @@
|
|
|
61
61
|
</p>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
|
-
<script>
|
|
65
|
-
|
|
64
|
+
<script type="module">
|
|
65
|
+
const versionSelect = document.getElementById('leaflet-version');
|
|
66
|
+
const params = new URLSearchParams(location.search);
|
|
67
|
+
versionSelect.value = params.get('leaflet') === '2' ? '2' : '1';
|
|
68
|
+
versionSelect.addEventListener('change', () => {
|
|
69
|
+
location.search = `?leaflet=${versionSelect.value}`;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const loadScript = src => new Promise((resolve, reject) => {
|
|
73
|
+
const script = document.createElement('script');
|
|
74
|
+
script.src = src;
|
|
75
|
+
script.onload = resolve;
|
|
76
|
+
script.onerror = () => reject(new Error(`Could not load ${src}`));
|
|
77
|
+
document.head.appendChild(script);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const version = versionSelect.value;
|
|
81
|
+
document.getElementById('leaflet-css').href = version === '2'
|
|
82
|
+
? 'https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.css'
|
|
83
|
+
: 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
|
84
|
+
|
|
85
|
+
if (version === '2') {
|
|
86
|
+
await loadScript('https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet-global-src.js');
|
|
87
|
+
globalThis.L = globalThis.leaflet;
|
|
88
|
+
} else {
|
|
89
|
+
await loadScript('https://unpkg.com/leaflet@1.9.4/dist/leaflet-src.js');
|
|
90
|
+
}
|
|
91
|
+
await loadScript('../dist/leaflet-rotate-src.js');
|
|
92
|
+
await loadScript('../lib/debug.js');
|
|
93
|
+
const L = globalThis.L;
|
|
94
|
+
|
|
95
|
+
const esri = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
66
96
|
id: 'mapbox.streets',
|
|
67
97
|
maxZoom: 24,
|
|
68
98
|
maxNativeZoom: 18,
|
|
@@ -70,14 +100,14 @@
|
|
|
70
100
|
// noWrap: true
|
|
71
101
|
});
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
const osm = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
74
104
|
maxZoom: 24,
|
|
75
105
|
maxNativeZoom: 19,
|
|
76
106
|
attribution: '© <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
77
107
|
// noWrap: true
|
|
78
108
|
});
|
|
79
109
|
|
|
80
|
-
|
|
110
|
+
const map = new L.Map('map', {
|
|
81
111
|
center: [55, 10],
|
|
82
112
|
zoom: 2,
|
|
83
113
|
layers: [esri],
|
|
@@ -99,6 +129,9 @@
|
|
|
99
129
|
touchRotate: true,
|
|
100
130
|
// touchZoom: true
|
|
101
131
|
});
|
|
132
|
+
window.map = map;
|
|
133
|
+
window.esri = esri;
|
|
134
|
+
window.osm = osm;
|
|
102
135
|
|
|
103
136
|
// map.setBearing(90);
|
|
104
137
|
// map.touchRotate.enable();
|
|
@@ -111,17 +144,18 @@
|
|
|
111
144
|
// map.rotateControl.setPosition('bottomleft');
|
|
112
145
|
// map.setMaxBounds([[-90,-180], [90,180]]);
|
|
113
146
|
|
|
114
|
-
|
|
115
|
-
'Empty': L.
|
|
147
|
+
const layers = new L.Control.Layers({
|
|
148
|
+
'Empty': new L.TileLayer(''),
|
|
116
149
|
'Streets': osm,
|
|
117
150
|
'Satellite': esri,
|
|
118
151
|
}, null, {
|
|
119
152
|
collapsed: false
|
|
120
153
|
}).addTo(map);
|
|
154
|
+
window.layers = layers;
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
for (
|
|
124
|
-
markers.push(L.
|
|
156
|
+
const markers = [];
|
|
157
|
+
for (const i in places) {
|
|
158
|
+
markers.push(new L.Marker(places[i], {
|
|
125
159
|
draggable: true
|
|
126
160
|
})
|
|
127
161
|
.bindPopup('<b>' + i + '</b><br>' + loremIpsum)
|
|
@@ -133,30 +167,34 @@
|
|
|
133
167
|
)
|
|
134
168
|
.addTo(map));
|
|
135
169
|
}
|
|
170
|
+
window.markers = markers;
|
|
136
171
|
|
|
137
|
-
|
|
138
|
-
renderer: L.
|
|
172
|
+
const path = new L.Polyline(route, {
|
|
173
|
+
renderer: new L.Canvas()
|
|
139
174
|
}).addTo(map);
|
|
175
|
+
window.path = path;
|
|
140
176
|
|
|
141
|
-
|
|
177
|
+
const circle = new L.Circle([53, 4], 111111)
|
|
142
178
|
.addTo(map);
|
|
179
|
+
window.circle = circle;
|
|
143
180
|
|
|
144
|
-
|
|
181
|
+
const polygon = new L.Polygon([
|
|
145
182
|
[48, -3],
|
|
146
183
|
[50, -4],
|
|
147
184
|
[52, 4]
|
|
148
185
|
]).addTo(map);
|
|
186
|
+
window.polygon = polygon;
|
|
149
187
|
|
|
150
|
-
const rectangle = L.
|
|
188
|
+
const rectangle = new L.Rectangle([
|
|
151
189
|
[60.81123, 25.71632],
|
|
152
190
|
[60.81028, 25.73166],
|
|
153
191
|
[60.7997, 25.72892],
|
|
154
192
|
[60.80061, 25.71362]
|
|
155
193
|
]).addTo(map);
|
|
194
|
+
window.rectangle = rectangle;
|
|
156
195
|
|
|
157
196
|
// Display some debug info
|
|
158
197
|
L.Rotate.debug(map);
|
|
159
|
-
|
|
160
198
|
</script>
|
|
161
199
|
|
|
162
200
|
<a href="https://github.com/Raruto/leaflet-rotate" class="view-on-github" style="position: fixed;bottom: 10px;left: calc(50% - 60px);z-index: 9999;"> <img alt="View on Github" src="https://raruto.github.io/img/view-on-github.png" title="View on Github"
|
|
@@ -71,10 +71,352 @@ Then('the bearing reaches 90 degrees gradually', function () {
|
|
|
71
71
|
assert.ok(Math.abs(this.animatedBearings.afterUnavailableHeading - 15) < 1e-6);
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
When('I set the map pitch to 45 degrees', async function () {
|
|
75
|
+
this.pitchProjection = await this.page.evaluate(() => {
|
|
76
|
+
const location = new L.LatLng(54, 12);
|
|
77
|
+
map.setView(new L.LatLng(55, 10), 5, { animate: false });
|
|
78
|
+
const flat = map.latLngToContainerPoint(location);
|
|
79
|
+
map.setPitch(90);
|
|
80
|
+
const maxPitch = map.getPitch();
|
|
81
|
+
map.setPitch(45);
|
|
82
|
+
const layerPoint = map.latLngToLayerPoint(location),
|
|
83
|
+
pitched = map.layerPointToContainerPoint(layerPoint),
|
|
84
|
+
roundTripPoint = map.containerPointToLayerPoint(pitched),
|
|
85
|
+
centerPoint = map.latLngToContainerPoint(map.getCenter()),
|
|
86
|
+
size = map.getSize();
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
pitch: map.getPitch(),
|
|
90
|
+
maxPitch,
|
|
91
|
+
bearing: map.getBearing(),
|
|
92
|
+
flat,
|
|
93
|
+
pitched,
|
|
94
|
+
layerPoint,
|
|
95
|
+
roundTripPoint,
|
|
96
|
+
centerPoint,
|
|
97
|
+
size,
|
|
98
|
+
transform: map._rotatePane.style.transform,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
Then('map coordinates stay consistent with the pitched view', function () {
|
|
104
|
+
const { pitch, maxPitch, bearing, flat, pitched, layerPoint, roundTripPoint, centerPoint, size, transform } = this.pitchProjection;
|
|
105
|
+
assert.equal(pitch, 45);
|
|
106
|
+
assert.equal(maxPitch, 85);
|
|
107
|
+
assert.ok(Math.abs(bearing - 30) < 1e-9);
|
|
108
|
+
assert.match(transform, /rotateX\(45deg\)/);
|
|
109
|
+
assert.ok(Math.abs(Number(transform.match(/perspective\(([\d.]+)px\)/)[1]) - size.y * 1.5) < 1);
|
|
110
|
+
assert.ok(Math.abs(flat.y - pitched.y) > 0.1);
|
|
111
|
+
const details = JSON.stringify({ flat, pitched, layerPoint, roundTripPoint, centerPoint, size });
|
|
112
|
+
assert.ok(Math.abs(roundTripPoint.x - layerPoint.x) < 1e-6, details);
|
|
113
|
+
assert.ok(Math.abs(roundTripPoint.y - layerPoint.y) < 1e-6, details);
|
|
114
|
+
assert.ok(Math.abs(centerPoint.x - size.x / 2) < 1, details);
|
|
115
|
+
assert.ok(Math.abs(centerPoint.y - size.y / 2) < 1, details);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
When('I change pitch and bearing with Ctrl-drag', async function () {
|
|
119
|
+
await this.page.evaluate(() => {
|
|
120
|
+
map.setPitch(0);
|
|
121
|
+
map.setBearing(0);
|
|
122
|
+
});
|
|
123
|
+
const bounds = await this.page.locator('#map').boundingBox();
|
|
124
|
+
const start = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 },
|
|
125
|
+
panePos = await this.page.evaluate(() => {
|
|
126
|
+
const pos = map._getMapPanePos();
|
|
127
|
+
return [pos.x, pos.y];
|
|
128
|
+
});
|
|
129
|
+
await this.page.keyboard.down('Control');
|
|
130
|
+
await this.page.mouse.move(start.x, start.y);
|
|
131
|
+
await this.page.mouse.down();
|
|
132
|
+
await this.page.mouse.move(start.x + 80, start.y - 80, { steps: 4 });
|
|
133
|
+
await this.page.mouse.up();
|
|
134
|
+
await this.page.keyboard.up('Control');
|
|
135
|
+
this.ctrlPitchDrag = await this.page.evaluate(() => {
|
|
136
|
+
const pane = map._getMapPanePos();
|
|
137
|
+
return {
|
|
138
|
+
pitch: map.getPitch(),
|
|
139
|
+
bearing: map.getBearing(),
|
|
140
|
+
finalPanePos: [pane.x, pane.y],
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
this.ctrlPitchDrag.panePos = panePos;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
Then('pitch and bearing change without moving the map pane', function () {
|
|
147
|
+
assert.equal(this.ctrlPitchDrag.pitch, 40);
|
|
148
|
+
assert.equal(this.ctrlPitchDrag.bearing, 64);
|
|
149
|
+
for (let i = 0; i < 2; i++) {
|
|
150
|
+
assert.ok(Math.abs(this.ctrlPitchDrag.panePos[i] - this.ctrlPitchDrag.finalPanePos[i]) < 0.01);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
When('I drag the pitched map', async function () {
|
|
155
|
+
const dragStart = await this.page.evaluate(() => {
|
|
156
|
+
map.setBearing(30);
|
|
157
|
+
map.setPitch(45);
|
|
158
|
+
map.setView(new L.LatLng(55, 10), 5, { animate: false });
|
|
159
|
+
const location = new L.LatLng(55, 10),
|
|
160
|
+
marker = new L.CircleMarker(location, { radius: 8 }).addTo(map),
|
|
161
|
+
pin = new L.Marker(location, {
|
|
162
|
+
icon: new L.DivIcon({ className: '', html: 'x', iconSize: [4, 4], iconAnchor: [2, 2] }),
|
|
163
|
+
}).addTo(map),
|
|
164
|
+
element = marker.getElement(),
|
|
165
|
+
pinElement = pin.getElement(),
|
|
166
|
+
container = map.getContainer(),
|
|
167
|
+
rect = container.getBoundingClientRect(),
|
|
168
|
+
start = [rect.left + rect.width / 2, rect.top + rect.height / 2];
|
|
169
|
+
const vectorScreenPoint = () => {
|
|
170
|
+
const bounds = element.getBBox();
|
|
171
|
+
const point = new DOMPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)
|
|
172
|
+
.matrixTransform(element.getScreenCTM());
|
|
173
|
+
return [point.x, point.y];
|
|
174
|
+
};
|
|
175
|
+
const before = vectorScreenPoint(),
|
|
176
|
+
pinBoundsBefore = pinElement.getBoundingClientRect(),
|
|
177
|
+
pinBefore = [pinBoundsBefore.left + pinBoundsBefore.width / 2, pinBoundsBefore.top + pinBoundsBefore.height / 2];
|
|
178
|
+
window.__pitchPanTest = { element, pinElement, vectorScreenPoint, before, pinBefore, rect };
|
|
179
|
+
return start;
|
|
180
|
+
});
|
|
181
|
+
await this.page.mouse.move(dragStart[0], dragStart[1]);
|
|
182
|
+
await this.page.mouse.down();
|
|
183
|
+
await this.page.mouse.move(dragStart[0] + 70, dragStart[1] + 35, { steps: 4 });
|
|
184
|
+
await this.page.mouse.up();
|
|
185
|
+
this.pitchedDrag = await this.page.evaluate(() => {
|
|
186
|
+
const { pinElement, vectorScreenPoint, before, pinBefore } = window.__pitchPanTest,
|
|
187
|
+
after = vectorScreenPoint(),
|
|
188
|
+
pinBoundsAfter = pinElement.getBoundingClientRect(),
|
|
189
|
+
pinAfter = [pinBoundsAfter.left + pinBoundsAfter.width / 2, pinBoundsAfter.top + pinBoundsAfter.height / 2];
|
|
190
|
+
delete window.__pitchPanTest;
|
|
191
|
+
return {
|
|
192
|
+
vectorDelta: [after[0] - before[0], after[1] - before[1]],
|
|
193
|
+
pinDelta: [pinAfter[0] - pinBefore[0], pinAfter[1] - pinBefore[1]],
|
|
194
|
+
expected: map.latLngToContainerPoint(new L.LatLng(55, 10)),
|
|
195
|
+
pointerDelta: [70, 35],
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
Then('vector layers stay aligned with their map coordinates', function () {
|
|
201
|
+
if (this.vectorAlignment) {
|
|
202
|
+
assert.ok(this.zoomAfterOffCenter > 5, 'Off-center wheel zoom did not run');
|
|
203
|
+
assert.ok(
|
|
204
|
+
this.pitchTransformError < 0.1,
|
|
205
|
+
`Pitch changed the projected map pane by ${this.pitchTransformError}px`
|
|
206
|
+
);
|
|
207
|
+
const centerDistance = Math.hypot(
|
|
208
|
+
this.centerBeforeCtrlDrag[0] - this.centerAfterCtrlDrag[0],
|
|
209
|
+
this.centerBeforeCtrlDrag[1] - this.centerAfterCtrlDrag[1]
|
|
210
|
+
);
|
|
211
|
+
assert.ok(centerDistance < 0.01, `Map center moved ${centerDistance} projected pixels`);
|
|
212
|
+
for (const layer of this.vectorAlignment) {
|
|
213
|
+
const error = Math.hypot(layer.actual[0] - layer.expected[0], layer.actual[1] - layer.expected[1]);
|
|
214
|
+
assert.ok(error < 2, `${layer.name} is ${error}px from its geographic position: ${JSON.stringify(layer)}`);
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { vectorDelta, pinDelta, pointerDelta } = this.pitchedDrag;
|
|
219
|
+
for (const delta of [vectorDelta, pinDelta]) {
|
|
220
|
+
assert.ok(Math.abs(delta[0] - pointerDelta[0]) < 2, JSON.stringify(this.pitchedDrag));
|
|
221
|
+
assert.ok(Math.abs(delta[1] - pointerDelta[1]) < 2, JSON.stringify(this.pitchedDrag));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
When('I zoom off-center then rotate the map', async function () {
|
|
226
|
+
await this.page.evaluate(() => {
|
|
227
|
+
map.setView(new L.LatLng(55, 10), 5, { animate: false });
|
|
228
|
+
map.setBearing(30);
|
|
229
|
+
map.setPitch(0);
|
|
230
|
+
});
|
|
231
|
+
const mapBounds = await this.page.locator('#map').boundingBox(),
|
|
232
|
+
zoomPoint = { x: mapBounds.x + mapBounds.width * 0.9, y: mapBounds.y + mapBounds.height * 0.1 };
|
|
233
|
+
await this.page.mouse.move(zoomPoint.x, zoomPoint.y);
|
|
234
|
+
await this.page.mouse.wheel(0, -480);
|
|
235
|
+
await this.page.waitForTimeout(500);
|
|
236
|
+
|
|
237
|
+
const positions = await this.page.evaluate(() => {
|
|
238
|
+
const location = map.containerPointToLatLng([600, 170]),
|
|
239
|
+
marker = new L.Marker(location, {
|
|
240
|
+
icon: new L.DivIcon({ className: '', html: 'x', iconSize: [8, 8], iconAnchor: [4, 4] }),
|
|
241
|
+
}).addTo(map),
|
|
242
|
+
circle = new L.CircleMarker(location, { radius: 5 }).addTo(map),
|
|
243
|
+
measure = () => {
|
|
244
|
+
const expected = map.latLngToContainerPoint(location),
|
|
245
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
246
|
+
vectorBounds = circle.getElement().getBoundingClientRect(),
|
|
247
|
+
markerBounds = marker.getElement().getBoundingClientRect();
|
|
248
|
+
return [
|
|
249
|
+
{
|
|
250
|
+
name: 'vector',
|
|
251
|
+
expected: [expected.x, expected.y],
|
|
252
|
+
actual: [vectorBounds.left + vectorBounds.width / 2 - container.left, vectorBounds.top + vectorBounds.height / 2 - container.top],
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'marker',
|
|
256
|
+
expected: [expected.x, expected.y],
|
|
257
|
+
actual: [markerBounds.left + markerBounds.width / 2 - container.left, markerBounds.top + markerBounds.height / 2 - container.top],
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const positions = [measure()];
|
|
263
|
+
const pane = map._rotatePane,
|
|
264
|
+
pivot = map._getPixelCenter(),
|
|
265
|
+
size = map.getSize(),
|
|
266
|
+
beforeTransform = new DOMMatrix(getComputedStyle(pane).transform),
|
|
267
|
+
testPoints = [[0, 0], [size.x, 0], [0, size.y], [size.x, size.y]]
|
|
268
|
+
.map(([x, y]) => ({
|
|
269
|
+
x,
|
|
270
|
+
y,
|
|
271
|
+
point: new DOMPoint(x, y).matrixTransform(beforeTransform),
|
|
272
|
+
}));
|
|
273
|
+
map.setPitch(1);
|
|
274
|
+
const afterTransform = new DOMMatrix(getComputedStyle(pane).transform),
|
|
275
|
+
pitch = Math.PI / 180,
|
|
276
|
+
perspective = size.y * 1.5;
|
|
277
|
+
const pitchTransformError = Math.max(...testPoints.map(({ x: pointX, y: pointY, point }) => {
|
|
278
|
+
const x = point.x / point.w - pivot.x,
|
|
279
|
+
y = point.y / point.w - pivot.y,
|
|
280
|
+
factor = perspective / (perspective - y * Math.sin(pitch)),
|
|
281
|
+
expectedX = pivot.x + x * factor,
|
|
282
|
+
expectedY = pivot.y + y * Math.cos(pitch) * factor,
|
|
283
|
+
actual = new DOMPoint(pointX, pointY).matrixTransform(afterTransform);
|
|
284
|
+
return Math.hypot(actual.x / actual.w - expectedX, actual.y / actual.w - expectedY);
|
|
285
|
+
}));
|
|
286
|
+
positions.push(measure());
|
|
287
|
+
map.setBearing(50);
|
|
288
|
+
positions.push(measure());
|
|
289
|
+
map.setPitch(35);
|
|
290
|
+
positions.push(measure());
|
|
291
|
+
map.panBy([50, 25], { animate: false });
|
|
292
|
+
positions.push(measure());
|
|
293
|
+
window.__rotateAlignment = { marker, circle, measure };
|
|
294
|
+
const center = map.project(map.getCenter());
|
|
295
|
+
return {
|
|
296
|
+
positions: positions.flatMap((position, index) => position.map(layer => ({ ...layer, step: index }))),
|
|
297
|
+
centerBeforeCtrlDrag: [center.x, center.y],
|
|
298
|
+
zoomAfterOffCenter: map.getZoom(),
|
|
299
|
+
pitchTransformError,
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
303
|
+
start = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
304
|
+
await this.page.keyboard.down('Control');
|
|
305
|
+
await this.page.mouse.move(start.x, start.y);
|
|
306
|
+
await this.page.mouse.down();
|
|
307
|
+
await this.page.mouse.move(start.x + 24, start.y - 18, { steps: 3 });
|
|
308
|
+
await this.page.mouse.up();
|
|
309
|
+
await this.page.keyboard.up('Control');
|
|
310
|
+
const afterCtrlDrag = await this.page.evaluate(() => {
|
|
311
|
+
const positions = window.__rotateAlignment.measure();
|
|
312
|
+
const center = map.project(map.getCenter());
|
|
313
|
+
window.__rotateAlignment.marker.remove();
|
|
314
|
+
window.__rotateAlignment.circle.remove();
|
|
315
|
+
delete window.__rotateAlignment;
|
|
316
|
+
return {
|
|
317
|
+
positions: positions.map(layer => ({ ...layer, step: 'Ctrl-drag' })),
|
|
318
|
+
center: [center.x, center.y],
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
this.vectorAlignment = [...positions.positions, ...afterCtrlDrag.positions];
|
|
322
|
+
this.centerBeforeCtrlDrag = positions.centerBeforeCtrlDrag;
|
|
323
|
+
this.centerAfterCtrlDrag = afterCtrlDrag.center;
|
|
324
|
+
this.zoomAfterOffCenter = positions.zoomAfterOffCenter;
|
|
325
|
+
this.pitchTransformError = positions.pitchTransformError;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
When('I pan the map then Ctrl-drag', async function () {
|
|
329
|
+
await this.page.evaluate(() => {
|
|
330
|
+
map.setView(new L.LatLng(55, 10), 5, { animate: false });
|
|
331
|
+
map.setBearing(30);
|
|
332
|
+
map.setPitch(0);
|
|
333
|
+
});
|
|
334
|
+
const bounds = await this.page.locator('#map').boundingBox();
|
|
335
|
+
const x = bounds.x + bounds.width / 2,
|
|
336
|
+
y = bounds.y + bounds.height / 2;
|
|
337
|
+
await this.page.mouse.move(x, y);
|
|
338
|
+
await this.page.mouse.down();
|
|
339
|
+
await this.page.mouse.move(x + 70, y + 35, { steps: 4 });
|
|
340
|
+
await this.page.mouse.up();
|
|
341
|
+
await this.page.waitForTimeout(400);
|
|
342
|
+
this.centerBeforeCtrlDrag = await this.page.evaluate(() => {
|
|
343
|
+
const center = map.project(map.getCenter()),
|
|
344
|
+
pane = map._getMapPanePos();
|
|
345
|
+
return { point: [center.x, center.y], pane: [pane.x, pane.y] };
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await this.page.keyboard.down('Control');
|
|
349
|
+
await this.page.mouse.move(x + 70, y + 35);
|
|
350
|
+
await this.page.mouse.down();
|
|
351
|
+
await this.page.mouse.move(x + 90, y + 20, { steps: 3 });
|
|
352
|
+
await this.page.mouse.up();
|
|
353
|
+
await this.page.keyboard.up('Control');
|
|
354
|
+
this.centerAfterCtrlDrag = await this.page.evaluate(() => {
|
|
355
|
+
const center = map.project(map.getCenter()),
|
|
356
|
+
pane = map._getMapPanePos();
|
|
357
|
+
return {
|
|
358
|
+
point: [center.x, center.y],
|
|
359
|
+
pane: [pane.x, pane.y],
|
|
360
|
+
pitch: map.getPitch(),
|
|
361
|
+
bearing: map.getBearing(),
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
Then('the map center stays stable during Ctrl-drag', function () {
|
|
367
|
+
const distance = Math.hypot(
|
|
368
|
+
this.centerBeforeCtrlDrag.point[0] - this.centerAfterCtrlDrag.point[0],
|
|
369
|
+
this.centerBeforeCtrlDrag.point[1] - this.centerAfterCtrlDrag.point[1]
|
|
370
|
+
);
|
|
371
|
+
assert.ok(distance < 0.01, `Map center moved ${distance} projected pixels: ${JSON.stringify({
|
|
372
|
+
before: this.centerBeforeCtrlDrag,
|
|
373
|
+
after: this.centerAfterCtrlDrag,
|
|
374
|
+
})}`);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
When('I Ctrl-drag while a wheel zoom is pending', async function () {
|
|
378
|
+
await this.page.evaluate(() => {
|
|
379
|
+
map.setView(new L.LatLng(55, 10), 5, { animate: false });
|
|
380
|
+
map.setBearing(0);
|
|
381
|
+
map.setPitch(0);
|
|
382
|
+
map.options.wheelDebounceTime = 500;
|
|
383
|
+
});
|
|
384
|
+
const bounds = await this.page.locator('#map').boundingBox();
|
|
385
|
+
const zoomPoint = { x: bounds.x + bounds.width * 0.9, y: bounds.y + bounds.height * 0.1 },
|
|
386
|
+
dragPoint = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
387
|
+
await this.page.mouse.move(zoomPoint.x, zoomPoint.y);
|
|
388
|
+
await this.page.mouse.wheel(0, -120);
|
|
389
|
+
this.pendingZoomBeforeDrag = await this.page.evaluate(() => ({
|
|
390
|
+
zoom: map.getZoom(),
|
|
391
|
+
center: map.project(map.getCenter()),
|
|
392
|
+
}));
|
|
393
|
+
|
|
394
|
+
await this.page.keyboard.down('Control');
|
|
395
|
+
await this.page.mouse.move(dragPoint.x, dragPoint.y);
|
|
396
|
+
await this.page.mouse.down();
|
|
397
|
+
await this.page.mouse.move(dragPoint.x + 20, dragPoint.y - 15);
|
|
398
|
+
await this.page.waitForTimeout(600);
|
|
399
|
+
this.pendingZoomAfterDrag = await this.page.evaluate(() => ({
|
|
400
|
+
zoom: map.getZoom(),
|
|
401
|
+
center: map.project(map.getCenter()),
|
|
402
|
+
}));
|
|
403
|
+
await this.page.mouse.up();
|
|
404
|
+
await this.page.keyboard.up('Control');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
Then('the pending wheel zoom does not move the map', function () {
|
|
408
|
+
assert.equal(this.pendingZoomAfterDrag.zoom, this.pendingZoomBeforeDrag.zoom);
|
|
409
|
+
const distance = Math.hypot(
|
|
410
|
+
this.pendingZoomBeforeDrag.center.x - this.pendingZoomAfterDrag.center.x,
|
|
411
|
+
this.pendingZoomBeforeDrag.center.y - this.pendingZoomAfterDrag.center.y
|
|
412
|
+
);
|
|
413
|
+
assert.ok(distance < 0.01, `Map center moved ${distance} projected pixels`);
|
|
414
|
+
});
|
|
415
|
+
|
|
74
416
|
When('I fit the same bounds twice', async function () {
|
|
75
417
|
this.zooms = await this.page.evaluate(() => {
|
|
76
418
|
const bounds = [[60.81123, 25.71632], [60.81028, 25.73166], [60.7997, 25.72892], [60.80061, 25.71362]];
|
|
77
|
-
L.
|
|
419
|
+
new L.Rectangle(bounds).addTo(map);
|
|
78
420
|
map.setZoom(0, { animate: false });
|
|
79
421
|
map.fitBounds(bounds, { animate: false });
|
|
80
422
|
const first = map.getZoom();
|
|
@@ -148,8 +490,8 @@ When('I rotate and zoom two fingers over a vector path', async function () {
|
|
|
148
490
|
this.gesturePathPositions = await this.page.evaluate(() => {
|
|
149
491
|
map.options.zoomAnimation = true;
|
|
150
492
|
map.setView([55, 10], 6, { animate: false });
|
|
151
|
-
const location = L.
|
|
152
|
-
const path = L.
|
|
493
|
+
const location = new L.LatLng(55.1, 10.1);
|
|
494
|
+
const path = new L.CircleMarker(location, { radius: 8 }).addTo(map);
|
|
153
495
|
const container = map.getContainer();
|
|
154
496
|
const center = map.getSize().divideBy(2);
|
|
155
497
|
const initial = map.getBearing(),
|
|
@@ -64,7 +64,7 @@ When('I rotate a map with the default rotate control', async function () {
|
|
|
64
64
|
const container = document.createElement('div');
|
|
65
65
|
container.style.cssText = 'width: 400px; height: 300px';
|
|
66
66
|
document.body.append(container);
|
|
67
|
-
const testMap = L.
|
|
67
|
+
const testMap = new L.Map(container, {
|
|
68
68
|
center: [55, 10],
|
|
69
69
|
zoom: 6,
|
|
70
70
|
rotate: true,
|
|
@@ -166,7 +166,7 @@ When('I apply compass heading readings', async function () {
|
|
|
166
166
|
const container = document.createElement('div');
|
|
167
167
|
container.style.cssText = 'width: 400px; height: 300px';
|
|
168
168
|
document.body.append(container);
|
|
169
|
-
const absoluteMap = L.
|
|
169
|
+
const absoluteMap = new L.Map(container, {
|
|
170
170
|
center: [55, 10],
|
|
171
171
|
zoom: 6,
|
|
172
172
|
rotate: true,
|
|
@@ -212,6 +212,50 @@ Then('valid compass readings update bearing and invalid readings are ignored', f
|
|
|
212
212
|
assert.ok(Math.abs(absoluteEventBearing - 60) < 1e-6);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
+
When('I use shift-wheel rotation with the handler disabled', async function () {
|
|
216
|
+
this.shiftWheelDisabled = await this.page.evaluate(async () => {
|
|
217
|
+
const container = document.createElement('div');
|
|
218
|
+
container.style.cssText = 'width: 400px; height: 300px';
|
|
219
|
+
document.body.append(container);
|
|
220
|
+
const testMap = new L.Map(container, {
|
|
221
|
+
center: [55, 10],
|
|
222
|
+
zoom: 6,
|
|
223
|
+
rotate: true,
|
|
224
|
+
rotateControl: false,
|
|
225
|
+
shiftKeyRotate: false,
|
|
226
|
+
zoomControl: false,
|
|
227
|
+
zoomAnimation: false,
|
|
228
|
+
});
|
|
229
|
+
const initialZoom = testMap.getZoom();
|
|
230
|
+
const initialBearing = testMap.getBearing();
|
|
231
|
+
container.dispatchEvent(new WheelEvent('wheel', {
|
|
232
|
+
bubbles: true,
|
|
233
|
+
cancelable: true,
|
|
234
|
+
deltaY: -60,
|
|
235
|
+
shiftKey: true,
|
|
236
|
+
}));
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
238
|
+
const result = {
|
|
239
|
+
handlerDisabled: !testMap.shiftKeyRotate.enabled(),
|
|
240
|
+
wheelZoomEnabled: testMap.scrollWheelZoom.enabled(),
|
|
241
|
+
zoomChanged: testMap.getZoom() > initialZoom,
|
|
242
|
+
bearingUnchanged: testMap.getBearing() === initialBearing,
|
|
243
|
+
};
|
|
244
|
+
testMap.remove();
|
|
245
|
+
container.remove();
|
|
246
|
+
return result;
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
Then('shift-wheel input zooms without changing the map bearing', function () {
|
|
251
|
+
assert.deepEqual(this.shiftWheelDisabled, {
|
|
252
|
+
handlerDisabled: true,
|
|
253
|
+
wheelZoomEnabled: true,
|
|
254
|
+
zoomChanged: true,
|
|
255
|
+
bearingUnchanged: true,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
215
259
|
When('I change the debugger bounds padding from the keyboard', async function () {
|
|
216
260
|
await this.page.evaluate(() => {
|
|
217
261
|
map.setView([55, 10], 6, { animate: false });
|
|
@@ -244,13 +288,30 @@ When('I inspect debugger geometry after resizing and moving the map', async func
|
|
|
244
288
|
map.panBy([20, 15], { animate: false });
|
|
245
289
|
map.setZoom(6, { animate: false });
|
|
246
290
|
window.debugMouseEvent = null;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
291
|
+
if (L.version.startsWith('2.')) {
|
|
292
|
+
map.getContainer().addEventListener('mousemove', event => {
|
|
293
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
294
|
+
const containerPoint = new L.Point(
|
|
295
|
+
event.clientX - rect.left - map.getContainer().clientLeft,
|
|
296
|
+
event.clientY - rect.top - map.getContainer().clientTop,
|
|
297
|
+
);
|
|
298
|
+
const latlng = map.containerPointToLatLng(containerPoint);
|
|
299
|
+
const layerPoint = map.containerPointToLayerPoint(containerPoint);
|
|
300
|
+
window.debugMouseEvent = {
|
|
301
|
+
latlng: [latlng.lng, latlng.lat],
|
|
302
|
+
layerPoint: [layerPoint.x, layerPoint.y],
|
|
303
|
+
containerPoint: [containerPoint.x, containerPoint.y],
|
|
304
|
+
};
|
|
305
|
+
}, { once: true });
|
|
306
|
+
} else {
|
|
307
|
+
map.once('mousemove', event => {
|
|
308
|
+
window.debugMouseEvent = {
|
|
309
|
+
latlng: [event.latlng.lng, event.latlng.lat],
|
|
310
|
+
layerPoint: [event.layerPoint.x, event.layerPoint.y],
|
|
311
|
+
containerPoint: [event.containerPoint.x, event.containerPoint.y],
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
254
315
|
});
|
|
255
316
|
|
|
256
317
|
const bounds = await this.page.locator('#map').boundingBox();
|
|
@@ -339,9 +400,9 @@ When('I rotate the example markers from the debugger', async function () {
|
|
|
339
400
|
When('I select {string} from the example page', async function (place) {
|
|
340
401
|
await this.page.getByRole('button', { name: place, exact: true }).click();
|
|
341
402
|
await this.page.waitForFunction(name =>
|
|
342
|
-
map.getZoom() === 10 && map.getCenter().distanceTo(L.
|
|
403
|
+
map.getZoom() === 10 && map.getCenter().distanceTo(new L.LatLng(...places[name])) < 1, place);
|
|
343
404
|
this.destination = await this.page.evaluate(name => ({
|
|
344
|
-
distance: map.getCenter().distanceTo(L.
|
|
405
|
+
distance: map.getCenter().distanceTo(new L.LatLng(...places[name])),
|
|
345
406
|
zoom: map.getZoom(),
|
|
346
407
|
}), place);
|
|
347
408
|
});
|