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
|
@@ -76,32 +76,62 @@ When('I rotate a map with the default rotate control', async function () {
|
|
|
76
76
|
const displayWhenRotated = getComputedStyle(control).display;
|
|
77
77
|
testMap.setBearing(0);
|
|
78
78
|
const displayAfterReset = getComputedStyle(control).display;
|
|
79
|
+
testMap.setPitch(30);
|
|
80
|
+
const displayWhenPitched = getComputedStyle(control).display;
|
|
81
|
+
testMap.setPitch(0);
|
|
82
|
+
const displayAfterPitchReset = getComputedStyle(control).display;
|
|
79
83
|
testMap.remove();
|
|
80
84
|
container.remove();
|
|
81
|
-
return [displayAtZero, displayWhenRotated, displayAfterReset];
|
|
85
|
+
return [displayAtZero, displayWhenRotated, displayAfterReset, displayWhenPitched, displayAfterPitchReset];
|
|
82
86
|
});
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
Then('the default rotate control visibility follows the bearing', function () {
|
|
86
|
-
assert.deepEqual(this.rotateControlVisibility, ['none', 'block', 'none']);
|
|
90
|
+
assert.deepEqual(this.rotateControlVisibility, ['none', 'block', 'none', 'block', 'none']);
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
When('I drag the rotate control', async function () {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
94
|
+
await this.page.evaluate(() => { map.setPitch(0); map.setBearing(0); });
|
|
95
|
+
const box = await this.page.locator('.leaflet-control-rotate-toggle').boundingBox(),
|
|
96
|
+
x = box.x + box.width / 2,
|
|
97
|
+
y = box.y + box.height / 2;
|
|
98
|
+
await this.page.mouse.move(x, y);
|
|
99
|
+
await this.page.mouse.down();
|
|
100
|
+
await this.page.mouse.move(x + 50, y, { steps: 5 });
|
|
101
|
+
await this.page.mouse.move(x + 50, y + 200, { steps: 5 });
|
|
102
|
+
await this.page.mouse.move(x + 50, y + 160, { steps: 5 });
|
|
103
|
+
await this.page.mouse.up();
|
|
104
|
+
await this.page.waitForTimeout(700);
|
|
105
|
+
this.controlDrag = await this.page.evaluate(() => ({
|
|
106
|
+
bearing: map.getBearing(),
|
|
107
|
+
pitch: map.getPitch(),
|
|
108
|
+
arrow: map.rotateControl._arrow.style.transform,
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
Then('the map bearing and pitch follow the control drag', function () {
|
|
113
|
+
// a drag must not be followed by the click reset
|
|
114
|
+
assert.ok(Math.abs(this.controlDrag.bearing - 40) < 1e-6, JSON.stringify(this.controlDrag));
|
|
115
|
+
assert.ok(Math.abs(this.controlDrag.pitch - 20) < 1e-6, JSON.stringify(this.controlDrag));
|
|
116
|
+
assert.match(this.controlDrag.arrow, /rotateX\((20|19\.9)[\d.]*deg\) rotate\((40|39\.9)[\d.]*deg\)/);
|
|
98
117
|
});
|
|
99
118
|
|
|
100
|
-
|
|
101
|
-
|
|
119
|
+
When('I click the rotate control on a rotated and pitched map', async function () {
|
|
120
|
+
await this.page.evaluate(() => { map.setBearing(120); map.setPitch(45); });
|
|
121
|
+
await this.page.locator('.leaflet-control-rotate-toggle').click();
|
|
122
|
+
this.controlReset = [await this.page.evaluate(() => ({ bearing: map.getBearing(), pitch: map.getPitch() }))];
|
|
123
|
+
await this.page.waitForTimeout(800);
|
|
124
|
+
this.controlReset.push(await this.page.evaluate(() => ({ bearing: map.getBearing(), pitch: map.getPitch() })));
|
|
102
125
|
});
|
|
103
126
|
|
|
104
|
-
|
|
127
|
+
Then('the map animates back to north and zero pitch', function () {
|
|
128
|
+
const [start, end] = this.controlReset;
|
|
129
|
+
assert.ok(start.bearing > 0 || start.pitch > 0, JSON.stringify(this.controlReset));
|
|
130
|
+
assert.equal(end.bearing, 0);
|
|
131
|
+
assert.equal(end.pitch, 0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
When('I enable compass bearing on a permission-based device', async function () {
|
|
105
135
|
this.permissionRequests = await this.page.evaluate(async () => {
|
|
106
136
|
const descriptor = Object.getOwnPropertyDescriptor(window, 'DeviceOrientationEvent');
|
|
107
137
|
window.permissionRequests = 0;
|
|
@@ -114,24 +144,21 @@ When('I activate compass mode on a permission-based device', async function () {
|
|
|
114
144
|
}
|
|
115
145
|
},
|
|
116
146
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
click();
|
|
122
|
-
click();
|
|
123
|
-
await Promise.resolve();
|
|
147
|
+
map.compassBearing.disable();
|
|
148
|
+
map.compassBearing.enable();
|
|
149
|
+
await new Promise(resolve => setTimeout(resolve));
|
|
150
|
+
const enabled = map.compassBearing.enabled();
|
|
124
151
|
if (descriptor) {
|
|
125
152
|
Object.defineProperty(window, 'DeviceOrientationEvent', descriptor);
|
|
126
153
|
} else {
|
|
127
154
|
delete window.DeviceOrientationEvent;
|
|
128
155
|
}
|
|
129
|
-
return window.permissionRequests;
|
|
156
|
+
return { requests: window.permissionRequests, enabled };
|
|
130
157
|
});
|
|
131
158
|
});
|
|
132
159
|
|
|
133
160
|
Then('device-orientation permission is requested', function () {
|
|
134
|
-
assert.
|
|
161
|
+
assert.deepEqual(this.permissionRequests, { requests: 1, enabled: false });
|
|
135
162
|
});
|
|
136
163
|
|
|
137
164
|
When('I apply compass heading readings', async function () {
|
|
@@ -327,8 +354,8 @@ When('I inspect debugger geometry after resizing and moving the map', async func
|
|
|
327
354
|
return [point.x - targets[index][0], point.y - targets[index][1]];
|
|
328
355
|
});
|
|
329
356
|
const pixelBounds = map.getPixelBounds();
|
|
330
|
-
const debugMin =
|
|
331
|
-
const debugMax =
|
|
357
|
+
const debugMin = document.getElementById('ogbx').textContent.split(',').map(Number);
|
|
358
|
+
const debugMax = document.getElementById('ogby').textContent.split(',').map(Number);
|
|
332
359
|
return {
|
|
333
360
|
size: [size.x, size.y],
|
|
334
361
|
paneSizes: ['.panebounds.rotate', '.panebounds.main'].map(selector => {
|
|
@@ -343,10 +370,10 @@ When('I inspect debugger geometry after resizing and moving the map', async func
|
|
|
343
370
|
],
|
|
344
371
|
pixelOrigin: [read('ogx') - map._pixelOrigin.x, read('ogy') - map._pixelOrigin.y],
|
|
345
372
|
pixelBounds: [
|
|
346
|
-
debugMin
|
|
347
|
-
debugMin
|
|
348
|
-
debugMax
|
|
349
|
-
debugMax
|
|
373
|
+
debugMin[0] - pixelBounds.min.x,
|
|
374
|
+
debugMin[1] - pixelBounds.min.y,
|
|
375
|
+
debugMax[0] - pixelBounds.max.x,
|
|
376
|
+
debugMax[1] - pixelBounds.max.y,
|
|
350
377
|
],
|
|
351
378
|
pointer: [
|
|
352
379
|
read('llx') - window.debugMouseEvent.latlng[0],
|
|
@@ -392,7 +419,7 @@ When('I use the example zoom controls', async function () {
|
|
|
392
419
|
|
|
393
420
|
When('I rotate the example markers from the debugger', async function () {
|
|
394
421
|
this.markerRotationsBefore = await this.page.evaluate(() => markers.map(marker => marker.options.rotation));
|
|
395
|
-
await this.page.getByRole('button', { name: 'Rotate
|
|
422
|
+
await this.page.getByRole('button', { name: 'Rotate', exact: true }).click();
|
|
396
423
|
await this.page.waitForTimeout(600);
|
|
397
424
|
this.markerRotationsAfter = await this.page.evaluate(() => markers.map(marker => marker.options.rotation));
|
|
398
425
|
});
|
|
@@ -53,6 +53,61 @@ When('I move a two-finger pinch gesture', async function () {
|
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
When('I drag two fingers vertically and then pinch with touch pitch enabled', async function () {
|
|
57
|
+
this.touchPitch = await this.page.evaluate(() => {
|
|
58
|
+
map.setView([55, 10], 6, { animate: false });
|
|
59
|
+
map.setPitch(0);
|
|
60
|
+
map.setBearing(20);
|
|
61
|
+
map.touchPitch.enable();
|
|
62
|
+
const container = map.getContainer();
|
|
63
|
+
const rect = container.getBoundingClientRect();
|
|
64
|
+
const centerX = rect.left + rect.width / 2;
|
|
65
|
+
const centerY = rect.top + rect.height / 2;
|
|
66
|
+
const touches = (x1, y1, x2, y2) => [
|
|
67
|
+
new Touch({ identifier: 1, target: container, clientX: centerX + x1, clientY: centerY + y1 }),
|
|
68
|
+
new Touch({ identifier: 2, target: container, clientX: centerX + x2, clientY: centerY + y2 }),
|
|
69
|
+
];
|
|
70
|
+
const dispatch = (target, type, activeTouches, changedTouches = activeTouches) => {
|
|
71
|
+
target.dispatchEvent(new TouchEvent(type, {
|
|
72
|
+
bubbles: true,
|
|
73
|
+
cancelable: true,
|
|
74
|
+
touches: activeTouches,
|
|
75
|
+
targetTouches: activeTouches,
|
|
76
|
+
changedTouches,
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
79
|
+
const state = () => ({ pitch: map.getPitch(), bearing: map.getBearing(), zoom: map.getZoom() });
|
|
80
|
+
|
|
81
|
+
// both fingers up by 60px: pitch +30°
|
|
82
|
+
dispatch(container, 'touchstart', touches(-40, 0, 40, 0));
|
|
83
|
+
for (let dy = -10; dy >= -60; dy -= 10) {
|
|
84
|
+
dispatch(document, 'touchmove', touches(-40, dy, 40, dy));
|
|
85
|
+
}
|
|
86
|
+
dispatch(document, 'touchend', [], touches(-40, -60, 40, -60));
|
|
87
|
+
const afterPitch = state();
|
|
88
|
+
|
|
89
|
+
// fingers spreading apart vertically: pinch, pitch unchanged
|
|
90
|
+
dispatch(container, 'touchstart', touches(-40, 0, 40, 0));
|
|
91
|
+
for (let d = 10; d <= 40; d += 10) {
|
|
92
|
+
dispatch(document, 'touchmove', touches(-40 - d, -d, 40 + d, d));
|
|
93
|
+
}
|
|
94
|
+
dispatch(document, 'touchend', [], touches(-80, -40, 80, 40));
|
|
95
|
+
const afterPinch = state();
|
|
96
|
+
|
|
97
|
+
map.touchPitch.disable();
|
|
98
|
+
map.setPitch(0);
|
|
99
|
+
return { afterPitch, afterPinch };
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
Then('only the vertical two-finger drag changes the pitch', function () {
|
|
104
|
+
const { afterPitch, afterPinch } = this.touchPitch;
|
|
105
|
+
assert.ok(Math.abs(afterPitch.pitch - 30) < 1e-6, JSON.stringify(this.touchPitch));
|
|
106
|
+
assert.ok(Math.abs(afterPitch.bearing - 20) < 1e-6, JSON.stringify(this.touchPitch));
|
|
107
|
+
assert.equal(afterPitch.zoom, 6);
|
|
108
|
+
assert.ok(Math.abs(afterPinch.pitch - 30) < 1e-6, JSON.stringify(this.touchPitch));
|
|
109
|
+
});
|
|
110
|
+
|
|
56
111
|
When('I drag a constrained marker on a rotated map', async function () {
|
|
57
112
|
await this.page.evaluate(() => {
|
|
58
113
|
map.options.zoomAnimation = false;
|
|
@@ -167,10 +167,8 @@ When('I drag the pitched map', async function () {
|
|
|
167
167
|
rect = container.getBoundingClientRect(),
|
|
168
168
|
start = [rect.left + rect.width / 2, rect.top + rect.height / 2];
|
|
169
169
|
const vectorScreenPoint = () => {
|
|
170
|
-
const bounds = element.
|
|
171
|
-
|
|
172
|
-
.matrixTransform(element.getScreenCTM());
|
|
173
|
-
return [point.x, point.y];
|
|
170
|
+
const bounds = element.getBoundingClientRect();
|
|
171
|
+
return [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2];
|
|
174
172
|
};
|
|
175
173
|
const before = vectorScreenPoint(),
|
|
176
174
|
pinBoundsBefore = pinElement.getBoundingClientRect(),
|
|
@@ -183,18 +181,42 @@ When('I drag the pitched map', async function () {
|
|
|
183
181
|
await this.page.mouse.move(dragStart[0] + 70, dragStart[1] + 35, { steps: 4 });
|
|
184
182
|
await this.page.mouse.up();
|
|
185
183
|
this.pitchedDrag = await this.page.evaluate(() => {
|
|
186
|
-
const { pinElement, vectorScreenPoint
|
|
184
|
+
const { pinElement, vectorScreenPoint } = window.__pitchPanTest,
|
|
187
185
|
after = vectorScreenPoint(),
|
|
188
186
|
pinBoundsAfter = pinElement.getBoundingClientRect(),
|
|
189
|
-
pinAfter = [pinBoundsAfter.left + pinBoundsAfter.width / 2, pinBoundsAfter.top + pinBoundsAfter.height / 2]
|
|
190
|
-
|
|
187
|
+
pinAfter = [pinBoundsAfter.left + pinBoundsAfter.width / 2, pinBoundsAfter.top + pinBoundsAfter.height / 2],
|
|
188
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
189
|
+
expected = map.latLngToContainerPoint(new L.LatLng(55, 10));
|
|
191
190
|
return {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
vector: [after[0] - container.left, after[1] - container.top],
|
|
192
|
+
expected: [expected.x, expected.y],
|
|
193
|
+
vectorError: Math.hypot(after[0] - container.left - expected.x, after[1] - container.top - expected.y),
|
|
194
|
+
pinError: Math.hypot(pinAfter[0] - container.left - expected.x, pinAfter[1] - container.top - expected.y),
|
|
196
195
|
};
|
|
197
196
|
});
|
|
197
|
+
this.repeatedPitchPanErrors = [];
|
|
198
|
+
for (let i = 0; i < 2; i++) {
|
|
199
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
200
|
+
center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
201
|
+
await this.page.mouse.move(center.x, center.y);
|
|
202
|
+
await this.page.mouse.down();
|
|
203
|
+
await this.page.mouse.move(center.x + 40, center.y + 20, { steps: 3 });
|
|
204
|
+
await this.page.mouse.up();
|
|
205
|
+
this.repeatedPitchPanErrors.push(await this.page.evaluate(() => {
|
|
206
|
+
const { element, pinElement } = window.__pitchPanTest,
|
|
207
|
+
bounds = element.getBoundingClientRect(),
|
|
208
|
+
vector = [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2],
|
|
209
|
+
pinBounds = pinElement.getBoundingClientRect(),
|
|
210
|
+
pin = [pinBounds.left + pinBounds.width / 2, pinBounds.top + pinBounds.height / 2],
|
|
211
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
212
|
+
expected = map.latLngToContainerPoint(new L.LatLng(55, 10));
|
|
213
|
+
return [
|
|
214
|
+
Math.hypot(vector[0] - container.left - expected.x, vector[1] - container.top - expected.y),
|
|
215
|
+
Math.hypot(pin[0] - container.left - expected.x, pin[1] - container.top - expected.y),
|
|
216
|
+
];
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
await this.page.evaluate(() => { delete window.__pitchPanTest; });
|
|
198
220
|
});
|
|
199
221
|
|
|
200
222
|
Then('vector layers stay aligned with their map coordinates', function () {
|
|
@@ -215,10 +237,92 @@ Then('vector layers stay aligned with their map coordinates', function () {
|
|
|
215
237
|
}
|
|
216
238
|
return;
|
|
217
239
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
assert.ok(
|
|
240
|
+
assert.ok(this.pitchedDrag.vectorError < 2, JSON.stringify(this.pitchedDrag));
|
|
241
|
+
assert.ok(this.pitchedDrag.pinError < 2, JSON.stringify(this.pitchedDrag));
|
|
242
|
+
for (const errors of this.repeatedPitchPanErrors) {
|
|
243
|
+
assert.ok(errors[0] < 2 && errors[1] < 2, JSON.stringify(this.repeatedPitchPanErrors));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
When('I pan a highly pitched map in several directions', { timeout: 30000 }, async function () {
|
|
248
|
+
this.pitchPanErrors = [];
|
|
249
|
+
this.page.on('pageerror', error => this.pitchPanErrors.push(error.message));
|
|
250
|
+
await this.page.evaluate(() => {
|
|
251
|
+
map.setView([50, 5], 5, { animate: false });
|
|
252
|
+
map.setBearing(45);
|
|
253
|
+
map.setPitch(70);
|
|
254
|
+
map.options.inertia = false;
|
|
255
|
+
const size = map.getSize(),
|
|
256
|
+
locations = [[0.35, 0.35], [0.65, 0.35], [0.35, 0.65], [0.65, 0.65]]
|
|
257
|
+
.map(([x, y]) => map.containerPointToLatLng([size.x * x, size.y * y])),
|
|
258
|
+
marker = new L.Marker(locations[0]).addTo(map),
|
|
259
|
+
vector = new L.CircleMarker(locations[0], { radius: 4 }).addTo(map),
|
|
260
|
+
bounds = element => {
|
|
261
|
+
const rect = element.getBoundingClientRect();
|
|
262
|
+
return [rect.x, rect.y, rect.width, rect.height];
|
|
263
|
+
};
|
|
264
|
+
window.__pitchRelease = {
|
|
265
|
+
locations,
|
|
266
|
+
resets: 0,
|
|
267
|
+
measure: () => ({
|
|
268
|
+
points: locations.map(ll => map.latLngToContainerPoint(ll)),
|
|
269
|
+
marker: bounds(marker.getElement()),
|
|
270
|
+
vector: bounds(vector.getElement()),
|
|
271
|
+
}),
|
|
272
|
+
};
|
|
273
|
+
map.on('viewreset', () => window.__pitchRelease.resets++);
|
|
274
|
+
});
|
|
275
|
+
this.pitchReleases = [];
|
|
276
|
+
for (const [dx, dy] of [[0, -60], [-60, 0], [30, 50], [30, 10]]) {
|
|
277
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
278
|
+
x = bounds.x + bounds.width / 2,
|
|
279
|
+
y = bounds.y + bounds.height / 2;
|
|
280
|
+
await this.page.mouse.move(x, y);
|
|
281
|
+
await this.page.mouse.down();
|
|
282
|
+
await this.page.mouse.move(x + dx, y + dy, { steps: 10 });
|
|
283
|
+
const before = await this.page.evaluate(() => window.__pitchRelease.measure());
|
|
284
|
+
await this.page.mouse.up();
|
|
285
|
+
await this.page.waitForTimeout(50);
|
|
286
|
+
this.pitchReleases.push(await this.page.evaluate(before => {
|
|
287
|
+
const state = window.__pitchRelease,
|
|
288
|
+
after = state.measure(),
|
|
289
|
+
center = map.project(map.getCenter()),
|
|
290
|
+
size = map.getSize(),
|
|
291
|
+
bearing = map.getBearing() * Math.PI / 180,
|
|
292
|
+
pitch = map.getPitch() * Math.PI / 180,
|
|
293
|
+
perspective = size.y * 1.5,
|
|
294
|
+
projectionErrors = state.locations.map(ll => {
|
|
295
|
+
const point = map.project(ll).subtract(center),
|
|
296
|
+
x = point.x * Math.cos(bearing) - point.y * Math.sin(bearing),
|
|
297
|
+
y = point.x * Math.sin(bearing) + point.y * Math.cos(bearing),
|
|
298
|
+
scale = perspective / (perspective - y * Math.sin(pitch)),
|
|
299
|
+
actual = map.latLngToContainerPoint(ll);
|
|
300
|
+
return Math.hypot(actual.x - size.x / 2 - x * scale, actual.y - size.y / 2 - y * Math.cos(pitch) * scale);
|
|
301
|
+
});
|
|
302
|
+
return { before, after, resets: state.resets, projectionErrors };
|
|
303
|
+
}, before));
|
|
304
|
+
}
|
|
305
|
+
this.pitchPanToError = await this.page.evaluate(() => {
|
|
306
|
+
const center = map.getCenter(),
|
|
307
|
+
target = new L.LatLng(center.lat + 0.1, center.lng + 0.1);
|
|
308
|
+
map.panTo(target, { animate: false });
|
|
309
|
+
return map.latLngToContainerPoint(target).distanceTo(map.getSize().divideBy(2));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
Then('releasing the map does not jump or reset its layers', function () {
|
|
314
|
+
assert.deepEqual(this.pitchPanErrors, [], 'Pitched dragging must not throw JavaScript errors');
|
|
315
|
+
assert.ok(this.pitchPanToError < 1.5, `panTo missed its target by ${this.pitchPanToError}px`);
|
|
316
|
+
for (const result of this.pitchReleases) {
|
|
317
|
+
assert.equal(result.resets, 0, 'Releasing a pan must not reset the view or reload its tiles');
|
|
318
|
+
for (let i = 0; i < result.before.points.length; i++) {
|
|
319
|
+
const before = result.before.points[i], after = result.after.points[i];
|
|
320
|
+
assert.ok(Math.hypot(before.x - after.x, before.y - after.y) < 0.1, JSON.stringify(result));
|
|
321
|
+
}
|
|
322
|
+
for (const layer of ['marker', 'vector']) {
|
|
323
|
+
assert.ok(result.before[layer].every((value, i) => Math.abs(value - result.after[layer][i]) < 0.1), JSON.stringify(result));
|
|
324
|
+
}
|
|
325
|
+
assert.ok(result.projectionErrors.every(error => error < 1.5), JSON.stringify(result));
|
|
222
326
|
}
|
|
223
327
|
});
|
|
224
328
|
|
|
@@ -575,3 +679,196 @@ Then('its drag callbacks match the marker and pointer coordinates', function ()
|
|
|
575
679
|
assert.ok(Math.abs(this.markerPosition.marker.lat - lastCallback[0]) < 1e-7, JSON.stringify(this.markerPosition));
|
|
576
680
|
assert.ok(Math.abs(this.markerPosition.marker.lng - lastCallback[1]) < 1e-7, JSON.stringify(this.markerPosition));
|
|
577
681
|
});
|
|
682
|
+
|
|
683
|
+
When('I animate a zoom from a low level on a rotated map', async function () {
|
|
684
|
+
this.lowZoomJump = await this.page.evaluate(async () => {
|
|
685
|
+
const style = document.createElement('style');
|
|
686
|
+
style.textContent = '*{transition:none!important}';
|
|
687
|
+
document.head.appendChild(style);
|
|
688
|
+
map.setView([50, 5], 1, { animate: false });
|
|
689
|
+
map.setBearing(30);
|
|
690
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
691
|
+
const element = circle.getElement(),
|
|
692
|
+
center = () => {
|
|
693
|
+
const bounds = element.getBoundingClientRect();
|
|
694
|
+
return [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2];
|
|
695
|
+
};
|
|
696
|
+
map.setZoom(3);
|
|
697
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
698
|
+
const animated = center();
|
|
699
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
700
|
+
const settled = center();
|
|
701
|
+
style.remove();
|
|
702
|
+
return Math.hypot(animated[0] - settled[0], animated[1] - settled[1]);
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
Then('vector layers do not jump at the end of the zoom animation', function () {
|
|
707
|
+
assert.ok(this.lowZoomJump < 3, `Vector layer jumped ${this.lowZoomJump}px`);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
When('I zoom around an off-center point on a pitched map', async function () {
|
|
711
|
+
this.zoomAroundDrift = await this.page.evaluate(async () => {
|
|
712
|
+
map.setView([50, 5], 6, { animate: false });
|
|
713
|
+
map.setBearing(30);
|
|
714
|
+
map.setPitch(60);
|
|
715
|
+
const size = map.getSize(),
|
|
716
|
+
point = new L.Point(size.x * 0.1, size.y * 0.9),
|
|
717
|
+
latlng = map.containerPointToLatLng(point);
|
|
718
|
+
map.setZoomAround(point, 7, { animate: false });
|
|
719
|
+
const drift = map.latLngToContainerPoint(latlng).distanceTo(point);
|
|
720
|
+
let animated = false;
|
|
721
|
+
map.once('zoomanim', () => animated = true);
|
|
722
|
+
map.setZoomAround(new L.Point(size.x * 0.85, size.y * 0.15), 8);
|
|
723
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
724
|
+
return { drift, animated };
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
Then('the point under the cursor stays in place', function () {
|
|
729
|
+
assert.ok(this.zoomAroundDrift.drift < 2, `Point under the cursor moved ${this.zoomAroundDrift.drift}px`);
|
|
730
|
+
assert.equal(this.zoomAroundDrift.animated, true, 'Zoom toward the horizon was not animated');
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
When('I right-drag horizontally below the map center', async function () {
|
|
734
|
+
await this.page.evaluate(() => {
|
|
735
|
+
map.setPitch(0);
|
|
736
|
+
map.setBearing(0);
|
|
737
|
+
window.__contextMenus = 0;
|
|
738
|
+
map.on('contextmenu', () => window.__contextMenus++);
|
|
739
|
+
});
|
|
740
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
741
|
+
start = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 + 50 };
|
|
742
|
+
await this.page.mouse.move(start.x, start.y);
|
|
743
|
+
await this.page.mouse.down({ button: 'right' });
|
|
744
|
+
await this.page.mouse.move(start.x + 50, start.y, { steps: 5 });
|
|
745
|
+
await this.page.mouse.up({ button: 'right' });
|
|
746
|
+
await this.page.locator('#map').dispatchEvent('contextmenu', { button: 2, clientX: start.x + 50, clientY: start.y });
|
|
747
|
+
this.rightDrag = await this.page.evaluate(() => ({
|
|
748
|
+
bearing: map.getBearing(),
|
|
749
|
+
contextMenus: window.__contextMenus,
|
|
750
|
+
}));
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
Then('the map rotates counterclockwise without opening the context menu', function () {
|
|
754
|
+
assert.ok(Math.abs(this.rightDrag.bearing - 320) < 1e-6, JSON.stringify(this.rightDrag));
|
|
755
|
+
assert.equal(this.rightDrag.contextMenus, 0);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
When('a context menu event fires immediately on right-button press', async function () {
|
|
759
|
+
this.earlyContextMenu = await this.page.evaluate(() => {
|
|
760
|
+
let contextMenus = 0;
|
|
761
|
+
map.on('contextmenu', () => contextMenus++);
|
|
762
|
+
const pointer = L.version.startsWith('2.');
|
|
763
|
+
const Event = pointer ? PointerEvent : MouseEvent;
|
|
764
|
+
map.getContainer().dispatchEvent(new Event(pointer ? 'pointerdown' : 'mousedown', {
|
|
765
|
+
button: 2, buttons: 2, bubbles: true, cancelable: true,
|
|
766
|
+
}));
|
|
767
|
+
const menu = new MouseEvent('contextmenu', { button: 2, bubbles: true, cancelable: true });
|
|
768
|
+
map.getContainer().dispatchEvent(menu);
|
|
769
|
+
document.dispatchEvent(new Event(pointer ? 'pointerup' : 'mouseup', { button: 2 }));
|
|
770
|
+
return { prevented: menu.defaultPrevented, contextMenus };
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
Then('the early context menu is suppressed', function () {
|
|
775
|
+
assert.deepEqual(this.earlyContextMenu, { prevented: true, contextMenus: 0 });
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
When('I look at a high zoom map with a high pitch', async function () {
|
|
779
|
+
this.highPitch = await this.page.evaluate(async () => {
|
|
780
|
+
const results = [];
|
|
781
|
+
for (const pitch of [70, 85]) {
|
|
782
|
+
map.setView([51.45, -0.26], 18, { animate: false });
|
|
783
|
+
map.setBearing(30);
|
|
784
|
+
map.setPitch(pitch);
|
|
785
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
786
|
+
const layer = Object.values(map._layers).find(l => l._tiles && l._map && l._tileZoom !== undefined),
|
|
787
|
+
tileSize = layer.getTileSize(),
|
|
788
|
+
size = map.getSize(),
|
|
789
|
+
farY = map._getPitchFarY();
|
|
790
|
+
let missing = 0;
|
|
791
|
+
for (let x = 0; x <= size.x; x += size.x / 8) {
|
|
792
|
+
for (let y = Math.max(0, farY + 2); y <= size.y; y += size.y / 8) {
|
|
793
|
+
const tile = map.project(map.containerPointToLatLng([x, y]), layer._tileZoom).unscaleBy(tileSize).floor();
|
|
794
|
+
if (!layer._tiles[`${tile.x}:${tile.y}:${layer._tileZoom}`]) missing++;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
const bounds = map.getBounds(), center = map.getCenter(), canvas = path._renderer._container;
|
|
798
|
+
results.push({
|
|
799
|
+
pitch,
|
|
800
|
+
farY,
|
|
801
|
+
missing,
|
|
802
|
+
tiles: Object.keys(layer._tiles).length,
|
|
803
|
+
canvas: [canvas.width, canvas.height],
|
|
804
|
+
boundsContainCenter: bounds.contains(center),
|
|
805
|
+
sky: map._pitchSky && map._pitchSky.style.display !== 'none',
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return results;
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
Then('tile loading stays bounded and covers the visible map', function () {
|
|
813
|
+
for (const result of this.highPitch) {
|
|
814
|
+
const details = JSON.stringify(result);
|
|
815
|
+
assert.equal(result.missing, 0, details);
|
|
816
|
+
assert.ok(result.tiles < 300, details);
|
|
817
|
+
assert.ok(Math.max(...result.canvas) < 5000, details);
|
|
818
|
+
assert.equal(result.boundsContainCenter, true, details);
|
|
819
|
+
assert.ok(result.farY > 0, details);
|
|
820
|
+
assert.equal(result.sky, true, details);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
When('I pan, zoom and change the pitch of a rotated map', async function () {
|
|
825
|
+
const page = this.page;
|
|
826
|
+
const setup = ([bearing, pitch]) => {
|
|
827
|
+
window.__tileErr = () => {
|
|
828
|
+
const layer = Object.values(map._layers).find(l => l._tiles && l._map && l._level),
|
|
829
|
+
level = layer._level,
|
|
830
|
+
point = new L.Point(150, 200),
|
|
831
|
+
ref = document.createElement('div'),
|
|
832
|
+
origin = map.project(map.containerPointToLatLng(point), level.zoom).round().subtract(level.origin);
|
|
833
|
+
ref.style.cssText = `position:absolute;width:0;height:0;left:${origin.x}px;top:${origin.y}px`;
|
|
834
|
+
level.el.appendChild(ref);
|
|
835
|
+
const rect = ref.getBoundingClientRect(), container = map.getContainer().getBoundingClientRect();
|
|
836
|
+
ref.remove();
|
|
837
|
+
return Math.hypot(rect.left - container.left - point.x, rect.top - container.top - point.y);
|
|
838
|
+
};
|
|
839
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
840
|
+
map.setBearing(bearing);
|
|
841
|
+
map.setPitch(pitch);
|
|
842
|
+
map.panBy([60, 60], { animate: false });
|
|
843
|
+
};
|
|
844
|
+
const box = await page.locator('#map').boundingBox();
|
|
845
|
+
this.tileAlignment = [];
|
|
846
|
+
for (const view of [[30, 30], [30, 0]]) {
|
|
847
|
+
await page.evaluate(setup, view);
|
|
848
|
+
await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.6);
|
|
849
|
+
for (const delta of [-200, 200]) {
|
|
850
|
+
const zoomEnd = page.evaluate(() => new Promise(resolve => { map.once('zoomend', () => resolve()); setTimeout(resolve, 2000); }));
|
|
851
|
+
await page.mouse.wheel(0, delta);
|
|
852
|
+
await zoomEnd;
|
|
853
|
+
await page.waitForTimeout(500);
|
|
854
|
+
this.tileAlignment.push({ view, delta, error: await page.evaluate(() => window.__tileErr()) });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
this.tileAlignment.push(await page.evaluate(async () => {
|
|
858
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
859
|
+
map.setPitch(85);
|
|
860
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
861
|
+
const centerError = map.project(map.getCenter()).distanceTo(map.project([51.45, -0.26]));
|
|
862
|
+
map.setPitch(0);
|
|
863
|
+
map.setBearing(30);
|
|
864
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
865
|
+
return { view: 'pitch 85 to 0', centerError, error: window.__tileErr() };
|
|
866
|
+
}));
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
Then('the map is centered and the tiles stay aligned with the map coordinates', function () {
|
|
870
|
+
for (const result of this.tileAlignment) {
|
|
871
|
+
assert.ok(result.error < 3, JSON.stringify(this.tileAlignment));
|
|
872
|
+
assert.ok(!(result.centerError > 2), JSON.stringify(this.tileAlignment));
|
|
873
|
+
}
|
|
874
|
+
});
|