leaflet-rotate 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -5
- package/dist/leaflet-rotate-src.js +1325 -254
- 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/misc/leaflet-heat-rotate.js +7 -0
- package/package.json +1 -1
- package/src/control/Rotate.js +160 -103
- package/src/dom/DomUtil.js +23 -12
- package/src/dom/Draggable.js +5 -0
- package/src/geo/LatLngBounds.js +5 -0
- package/src/geometry/Bounds.js +5 -0
- package/src/geometry/Point.js +17 -3
- package/src/index.js +4 -1
- package/src/layer/DivOverlay.js +34 -5
- package/src/layer/Popup.js +14 -3
- package/src/layer/Tooltip.js +16 -0
- package/src/layer/marker/Icon.js +12 -0
- package/src/layer/marker/Marker.js +86 -7
- package/src/layer/tile/GridLayer.js +38 -3
- package/src/layer/vector/Renderer.js +88 -34
- package/src/map/Map.js +578 -116
- package/src/map/handler/CompassBearing.js +34 -1
- package/src/map/handler/ContainerMutation.js +18 -1
- package/src/map/handler/ShiftKeyRotate.js +204 -13
- package/src/map/handler/TouchGestures.js +108 -10
- package/src/map/handler/TouchPitch.js +53 -0
- package/src/map/handler/TouchRotate.js +18 -1
- package/src/map/handler/TouchZoom.js +21 -1
- package/test/features/example.feature +8 -4
- package/test/features/gestures.feature +4 -0
- package/test/features/rotation.feature +60 -0
- package/test/index.html +342 -186
- package/test/steps/bearing.steps.js +535 -0
- package/test/steps/example.steps.js +56 -29
- package/test/steps/gestures.steps.js +55 -0
- package/test/support/hooks.js +4 -16
|
@@ -71,6 +71,348 @@ 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]];
|
|
@@ -233,3 +575,196 @@ Then('its drag callbacks match the marker and pointer coordinates', function ()
|
|
|
233
575
|
assert.ok(Math.abs(this.markerPosition.marker.lat - lastCallback[0]) < 1e-7, JSON.stringify(this.markerPosition));
|
|
234
576
|
assert.ok(Math.abs(this.markerPosition.marker.lng - lastCallback[1]) < 1e-7, JSON.stringify(this.markerPosition));
|
|
235
577
|
});
|
|
578
|
+
|
|
579
|
+
When('I animate a zoom from a low level on a rotated map', async function () {
|
|
580
|
+
this.lowZoomJump = await this.page.evaluate(async () => {
|
|
581
|
+
const style = document.createElement('style');
|
|
582
|
+
style.textContent = '*{transition:none!important}';
|
|
583
|
+
document.head.appendChild(style);
|
|
584
|
+
map.setView([50, 5], 1, { animate: false });
|
|
585
|
+
map.setBearing(30);
|
|
586
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
587
|
+
const element = circle.getElement(),
|
|
588
|
+
center = () => {
|
|
589
|
+
const bounds = element.getBoundingClientRect();
|
|
590
|
+
return [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2];
|
|
591
|
+
};
|
|
592
|
+
map.setZoom(3);
|
|
593
|
+
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
594
|
+
const animated = center();
|
|
595
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
596
|
+
const settled = center();
|
|
597
|
+
style.remove();
|
|
598
|
+
return Math.hypot(animated[0] - settled[0], animated[1] - settled[1]);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
Then('vector layers do not jump at the end of the zoom animation', function () {
|
|
603
|
+
assert.ok(this.lowZoomJump < 3, `Vector layer jumped ${this.lowZoomJump}px`);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
When('I zoom around an off-center point on a pitched map', async function () {
|
|
607
|
+
this.zoomAroundDrift = await this.page.evaluate(async () => {
|
|
608
|
+
map.setView([50, 5], 6, { animate: false });
|
|
609
|
+
map.setBearing(30);
|
|
610
|
+
map.setPitch(60);
|
|
611
|
+
const size = map.getSize(),
|
|
612
|
+
point = new L.Point(size.x * 0.1, size.y * 0.9),
|
|
613
|
+
latlng = map.containerPointToLatLng(point);
|
|
614
|
+
map.setZoomAround(point, 7, { animate: false });
|
|
615
|
+
const drift = map.latLngToContainerPoint(latlng).distanceTo(point);
|
|
616
|
+
let animated = false;
|
|
617
|
+
map.once('zoomanim', () => animated = true);
|
|
618
|
+
map.setZoomAround(new L.Point(size.x * 0.85, size.y * 0.15), 8);
|
|
619
|
+
await new Promise(resolve => setTimeout(resolve, 400));
|
|
620
|
+
return { drift, animated };
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
Then('the point under the cursor stays in place', function () {
|
|
625
|
+
assert.ok(this.zoomAroundDrift.drift < 2, `Point under the cursor moved ${this.zoomAroundDrift.drift}px`);
|
|
626
|
+
assert.equal(this.zoomAroundDrift.animated, true, 'Zoom toward the horizon was not animated');
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
When('I right-drag horizontally below the map center', async function () {
|
|
630
|
+
await this.page.evaluate(() => {
|
|
631
|
+
map.setPitch(0);
|
|
632
|
+
map.setBearing(0);
|
|
633
|
+
window.__contextMenus = 0;
|
|
634
|
+
map.on('contextmenu', () => window.__contextMenus++);
|
|
635
|
+
});
|
|
636
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
637
|
+
start = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 + 50 };
|
|
638
|
+
await this.page.mouse.move(start.x, start.y);
|
|
639
|
+
await this.page.mouse.down({ button: 'right' });
|
|
640
|
+
await this.page.mouse.move(start.x + 50, start.y, { steps: 5 });
|
|
641
|
+
await this.page.mouse.up({ button: 'right' });
|
|
642
|
+
await this.page.locator('#map').dispatchEvent('contextmenu', { button: 2, clientX: start.x + 50, clientY: start.y });
|
|
643
|
+
this.rightDrag = await this.page.evaluate(() => ({
|
|
644
|
+
bearing: map.getBearing(),
|
|
645
|
+
contextMenus: window.__contextMenus,
|
|
646
|
+
}));
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
Then('the map rotates counterclockwise without opening the context menu', function () {
|
|
650
|
+
assert.ok(Math.abs(this.rightDrag.bearing - 320) < 1e-6, JSON.stringify(this.rightDrag));
|
|
651
|
+
assert.equal(this.rightDrag.contextMenus, 0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
When('a context menu event fires immediately on right-button press', async function () {
|
|
655
|
+
this.earlyContextMenu = await this.page.evaluate(() => {
|
|
656
|
+
let contextMenus = 0;
|
|
657
|
+
map.on('contextmenu', () => contextMenus++);
|
|
658
|
+
const pointer = L.version.startsWith('2.');
|
|
659
|
+
const Event = pointer ? PointerEvent : MouseEvent;
|
|
660
|
+
map.getContainer().dispatchEvent(new Event(pointer ? 'pointerdown' : 'mousedown', {
|
|
661
|
+
button: 2, buttons: 2, bubbles: true, cancelable: true,
|
|
662
|
+
}));
|
|
663
|
+
const menu = new MouseEvent('contextmenu', { button: 2, bubbles: true, cancelable: true });
|
|
664
|
+
map.getContainer().dispatchEvent(menu);
|
|
665
|
+
document.dispatchEvent(new Event(pointer ? 'pointerup' : 'mouseup', { button: 2 }));
|
|
666
|
+
return { prevented: menu.defaultPrevented, contextMenus };
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
Then('the early context menu is suppressed', function () {
|
|
671
|
+
assert.deepEqual(this.earlyContextMenu, { prevented: true, contextMenus: 0 });
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
When('I look at a high zoom map with a high pitch', async function () {
|
|
675
|
+
this.highPitch = await this.page.evaluate(async () => {
|
|
676
|
+
const results = [];
|
|
677
|
+
for (const pitch of [70, 85]) {
|
|
678
|
+
map.setView([51.45, -0.26], 18, { animate: false });
|
|
679
|
+
map.setBearing(30);
|
|
680
|
+
map.setPitch(pitch);
|
|
681
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
682
|
+
const layer = Object.values(map._layers).find(l => l._tiles && l._map && l._tileZoom !== undefined),
|
|
683
|
+
tileSize = layer.getTileSize(),
|
|
684
|
+
size = map.getSize(),
|
|
685
|
+
farY = map._getPitchFarY();
|
|
686
|
+
let missing = 0;
|
|
687
|
+
for (let x = 0; x <= size.x; x += size.x / 8) {
|
|
688
|
+
for (let y = Math.max(0, farY + 2); y <= size.y; y += size.y / 8) {
|
|
689
|
+
const tile = map.project(map.containerPointToLatLng([x, y]), layer._tileZoom).unscaleBy(tileSize).floor();
|
|
690
|
+
if (!layer._tiles[`${tile.x}:${tile.y}:${layer._tileZoom}`]) missing++;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const bounds = map.getBounds(), center = map.getCenter(), canvas = path._renderer._container;
|
|
694
|
+
results.push({
|
|
695
|
+
pitch,
|
|
696
|
+
farY,
|
|
697
|
+
missing,
|
|
698
|
+
tiles: Object.keys(layer._tiles).length,
|
|
699
|
+
canvas: [canvas.width, canvas.height],
|
|
700
|
+
boundsContainCenter: bounds.contains(center),
|
|
701
|
+
sky: map._pitchSky && map._pitchSky.style.display !== 'none',
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return results;
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
Then('tile loading stays bounded and covers the visible map', function () {
|
|
709
|
+
for (const result of this.highPitch) {
|
|
710
|
+
const details = JSON.stringify(result);
|
|
711
|
+
assert.equal(result.missing, 0, details);
|
|
712
|
+
assert.ok(result.tiles < 300, details);
|
|
713
|
+
assert.ok(Math.max(...result.canvas) < 5000, details);
|
|
714
|
+
assert.equal(result.boundsContainCenter, true, details);
|
|
715
|
+
assert.ok(result.farY > 0, details);
|
|
716
|
+
assert.equal(result.sky, true, details);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
When('I pan, zoom and change the pitch of a rotated map', async function () {
|
|
721
|
+
const page = this.page;
|
|
722
|
+
const setup = ([bearing, pitch]) => {
|
|
723
|
+
window.__tileErr = () => {
|
|
724
|
+
const layer = Object.values(map._layers).find(l => l._tiles && l._map && l._level),
|
|
725
|
+
level = layer._level,
|
|
726
|
+
point = new L.Point(150, 200),
|
|
727
|
+
ref = document.createElement('div'),
|
|
728
|
+
origin = map.project(map.containerPointToLatLng(point), level.zoom).round().subtract(level.origin);
|
|
729
|
+
ref.style.cssText = `position:absolute;width:0;height:0;left:${origin.x}px;top:${origin.y}px`;
|
|
730
|
+
level.el.appendChild(ref);
|
|
731
|
+
const rect = ref.getBoundingClientRect(), container = map.getContainer().getBoundingClientRect();
|
|
732
|
+
ref.remove();
|
|
733
|
+
return Math.hypot(rect.left - container.left - point.x, rect.top - container.top - point.y);
|
|
734
|
+
};
|
|
735
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
736
|
+
map.setBearing(bearing);
|
|
737
|
+
map.setPitch(pitch);
|
|
738
|
+
map.panBy([60, 60], { animate: false });
|
|
739
|
+
};
|
|
740
|
+
const box = await page.locator('#map').boundingBox();
|
|
741
|
+
this.tileAlignment = [];
|
|
742
|
+
for (const view of [[30, 30], [30, 0]]) {
|
|
743
|
+
await page.evaluate(setup, view);
|
|
744
|
+
await page.mouse.move(box.x + box.width * 0.7, box.y + box.height * 0.6);
|
|
745
|
+
for (const delta of [-200, 200]) {
|
|
746
|
+
const zoomEnd = page.evaluate(() => new Promise(resolve => { map.once('zoomend', () => resolve()); setTimeout(resolve, 2000); }));
|
|
747
|
+
await page.mouse.wheel(0, delta);
|
|
748
|
+
await zoomEnd;
|
|
749
|
+
await page.waitForTimeout(500);
|
|
750
|
+
this.tileAlignment.push({ view, delta, error: await page.evaluate(() => window.__tileErr()) });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
this.tileAlignment.push(await page.evaluate(async () => {
|
|
754
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
755
|
+
map.setPitch(85);
|
|
756
|
+
map.setView([51.45, -0.26], 10, { animate: false });
|
|
757
|
+
const centerError = map.project(map.getCenter()).distanceTo(map.project([51.45, -0.26]));
|
|
758
|
+
map.setPitch(0);
|
|
759
|
+
map.setBearing(30);
|
|
760
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
761
|
+
return { view: 'pitch 85 to 0', centerError, error: window.__tileErr() };
|
|
762
|
+
}));
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
Then('the map is centered and the tiles stay aligned with the map coordinates', function () {
|
|
766
|
+
for (const result of this.tileAlignment) {
|
|
767
|
+
assert.ok(result.error < 3, JSON.stringify(this.tileAlignment));
|
|
768
|
+
assert.ok(!(result.centerError > 2), JSON.stringify(this.tileAlignment));
|
|
769
|
+
}
|
|
770
|
+
});
|
|
@@ -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
|
});
|