leaflet-rotate 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/build/server.js +19 -3
- package/dist/leaflet-rotate-src.js +313 -73
- 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/package.json +1 -1
- package/src/index.js +1 -0
- package/src/layer/marker/Marker.js +24 -11
- package/src/layer/tile/GridLayer.js +16 -1
- package/src/map/Map.js +270 -61
- package/src/map/handler/Drag.js +59 -0
- package/test/features/navigation-bounds.feature +33 -0
- package/test/features/overlays.feature +9 -0
- package/test/features/pitch.feature +19 -0
- package/test/features/rotation.feature +5 -0
- package/test/index.html +312 -29
- package/test/steps/navigation-bounds.steps.js +195 -0
- package/test/steps/overlays.steps.js +44 -0
- package/test/steps/pitch.steps.js +264 -0
- package/test/steps/{bearing.steps.js → rotation.steps.js} +147 -19
- package/test/support/hooks.js +19 -7
|
@@ -84,6 +84,28 @@ When('I set the map pitch to 45 degrees', async function () {
|
|
|
84
84
|
roundTripPoint = map.containerPointToLayerPoint(pitched),
|
|
85
85
|
centerPoint = map.latLngToContainerPoint(map.getCenter()),
|
|
86
86
|
size = map.getSize();
|
|
87
|
+
const css = new DOMMatrix(getComputedStyle(map._rotatePane).transform),
|
|
88
|
+
pivot = map._getPixelCenter(),
|
|
89
|
+
origin = map._rotatePane._leaflet_pos,
|
|
90
|
+
angle = Math.PI / 4,
|
|
91
|
+
bearing = map._bearing,
|
|
92
|
+
perspective = size.y * 1.5,
|
|
93
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
94
|
+
panePos = map._getMapPanePos(),
|
|
95
|
+
projectionError = Math.max(...[[0, 0], [size.x, 0], [0, size.y], [size.x, size.y]].map(([x, y]) => {
|
|
96
|
+
const dx = x + origin.x - pivot.x,
|
|
97
|
+
dy = y + origin.y - pivot.y,
|
|
98
|
+
rotatedX = dx * Math.cos(bearing) - dy * Math.sin(bearing),
|
|
99
|
+
rotatedY = dx * Math.sin(bearing) + dy * Math.cos(bearing),
|
|
100
|
+
factor = perspective / (perspective - rotatedY * Math.sin(angle)),
|
|
101
|
+
probe = document.createElement('div');
|
|
102
|
+
probe.style.cssText = `position:absolute;left:${x}px;top:${y}px;width:0;height:0`;
|
|
103
|
+
map._rotatePane.appendChild(probe);
|
|
104
|
+
const actual = probe.getBoundingClientRect();
|
|
105
|
+
probe.remove();
|
|
106
|
+
return Math.hypot(actual.x - container.left - map.getContainer().clientLeft - panePos.x - pivot.x - rotatedX * factor,
|
|
107
|
+
actual.y - container.top - map.getContainer().clientTop - panePos.y - pivot.y - rotatedY * Math.cos(angle) * factor);
|
|
108
|
+
}));
|
|
87
109
|
|
|
88
110
|
return {
|
|
89
111
|
pitch: map.getPitch(),
|
|
@@ -95,18 +117,20 @@ When('I set the map pitch to 45 degrees', async function () {
|
|
|
95
117
|
roundTripPoint,
|
|
96
118
|
centerPoint,
|
|
97
119
|
size,
|
|
98
|
-
|
|
120
|
+
projectionError,
|
|
121
|
+
groundDepth: [css.m13, css.m23, css.m43],
|
|
99
122
|
};
|
|
100
123
|
});
|
|
101
124
|
});
|
|
102
125
|
|
|
103
126
|
Then('map coordinates stay consistent with the pitched view', function () {
|
|
104
|
-
const { pitch, maxPitch, bearing, flat, pitched, layerPoint, roundTripPoint, centerPoint, size,
|
|
127
|
+
const { pitch, maxPitch, bearing, flat, pitched, layerPoint, roundTripPoint, centerPoint, size,
|
|
128
|
+
projectionError, groundDepth } = this.pitchProjection;
|
|
105
129
|
assert.equal(pitch, 45);
|
|
106
130
|
assert.equal(maxPitch, 85);
|
|
107
131
|
assert.ok(Math.abs(bearing - 30) < 1e-9);
|
|
108
|
-
assert.
|
|
109
|
-
assert.
|
|
132
|
+
assert.ok(projectionError < 0.01, `CSS pitch projection error: ${projectionError}px`);
|
|
133
|
+
assert.deepEqual(groundDepth, [0, 0, 0], 'Ground must not occlude upright overlays');
|
|
110
134
|
assert.ok(Math.abs(flat.y - pitched.y) > 0.1);
|
|
111
135
|
const details = JSON.stringify({ flat, pitched, layerPoint, roundTripPoint, centerPoint, size });
|
|
112
136
|
assert.ok(Math.abs(roundTripPoint.x - layerPoint.x) < 1e-6, details);
|
|
@@ -167,10 +191,8 @@ When('I drag the pitched map', async function () {
|
|
|
167
191
|
rect = container.getBoundingClientRect(),
|
|
168
192
|
start = [rect.left + rect.width / 2, rect.top + rect.height / 2];
|
|
169
193
|
const vectorScreenPoint = () => {
|
|
170
|
-
const bounds = element.
|
|
171
|
-
|
|
172
|
-
.matrixTransform(element.getScreenCTM());
|
|
173
|
-
return [point.x, point.y];
|
|
194
|
+
const bounds = element.getBoundingClientRect();
|
|
195
|
+
return [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2];
|
|
174
196
|
};
|
|
175
197
|
const before = vectorScreenPoint(),
|
|
176
198
|
pinBoundsBefore = pinElement.getBoundingClientRect(),
|
|
@@ -183,18 +205,42 @@ When('I drag the pitched map', async function () {
|
|
|
183
205
|
await this.page.mouse.move(dragStart[0] + 70, dragStart[1] + 35, { steps: 4 });
|
|
184
206
|
await this.page.mouse.up();
|
|
185
207
|
this.pitchedDrag = await this.page.evaluate(() => {
|
|
186
|
-
const { pinElement, vectorScreenPoint
|
|
208
|
+
const { pinElement, vectorScreenPoint } = window.__pitchPanTest,
|
|
187
209
|
after = vectorScreenPoint(),
|
|
188
210
|
pinBoundsAfter = pinElement.getBoundingClientRect(),
|
|
189
|
-
pinAfter = [pinBoundsAfter.left + pinBoundsAfter.width / 2, pinBoundsAfter.top + pinBoundsAfter.height / 2]
|
|
190
|
-
|
|
211
|
+
pinAfter = [pinBoundsAfter.left + pinBoundsAfter.width / 2, pinBoundsAfter.top + pinBoundsAfter.height / 2],
|
|
212
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
213
|
+
expected = map.latLngToContainerPoint(new L.LatLng(55, 10));
|
|
191
214
|
return {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
215
|
+
vector: [after[0] - container.left, after[1] - container.top],
|
|
216
|
+
expected: [expected.x, expected.y],
|
|
217
|
+
vectorError: Math.hypot(after[0] - container.left - expected.x, after[1] - container.top - expected.y),
|
|
218
|
+
pinError: Math.hypot(pinAfter[0] - container.left - expected.x, pinAfter[1] - container.top - expected.y),
|
|
196
219
|
};
|
|
197
220
|
});
|
|
221
|
+
this.repeatedPitchPanErrors = [];
|
|
222
|
+
for (let i = 0; i < 2; i++) {
|
|
223
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
224
|
+
center = { x: bounds.x + bounds.width / 2, y: bounds.y + bounds.height / 2 };
|
|
225
|
+
await this.page.mouse.move(center.x, center.y);
|
|
226
|
+
await this.page.mouse.down();
|
|
227
|
+
await this.page.mouse.move(center.x + 40, center.y + 20, { steps: 3 });
|
|
228
|
+
await this.page.mouse.up();
|
|
229
|
+
this.repeatedPitchPanErrors.push(await this.page.evaluate(() => {
|
|
230
|
+
const { element, pinElement } = window.__pitchPanTest,
|
|
231
|
+
bounds = element.getBoundingClientRect(),
|
|
232
|
+
vector = [bounds.left + bounds.width / 2, bounds.top + bounds.height / 2],
|
|
233
|
+
pinBounds = pinElement.getBoundingClientRect(),
|
|
234
|
+
pin = [pinBounds.left + pinBounds.width / 2, pinBounds.top + pinBounds.height / 2],
|
|
235
|
+
container = map.getContainer().getBoundingClientRect(),
|
|
236
|
+
expected = map.latLngToContainerPoint(new L.LatLng(55, 10));
|
|
237
|
+
return [
|
|
238
|
+
Math.hypot(vector[0] - container.left - expected.x, vector[1] - container.top - expected.y),
|
|
239
|
+
Math.hypot(pin[0] - container.left - expected.x, pin[1] - container.top - expected.y),
|
|
240
|
+
];
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
await this.page.evaluate(() => { delete window.__pitchPanTest; });
|
|
198
244
|
});
|
|
199
245
|
|
|
200
246
|
Then('vector layers stay aligned with their map coordinates', function () {
|
|
@@ -215,10 +261,92 @@ Then('vector layers stay aligned with their map coordinates', function () {
|
|
|
215
261
|
}
|
|
216
262
|
return;
|
|
217
263
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
assert.ok(
|
|
264
|
+
assert.ok(this.pitchedDrag.vectorError < 2, JSON.stringify(this.pitchedDrag));
|
|
265
|
+
assert.ok(this.pitchedDrag.pinError < 2, JSON.stringify(this.pitchedDrag));
|
|
266
|
+
for (const errors of this.repeatedPitchPanErrors) {
|
|
267
|
+
assert.ok(errors[0] < 2 && errors[1] < 2, JSON.stringify(this.repeatedPitchPanErrors));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
When('I pan a highly pitched map in several directions', { timeout: 30000 }, async function () {
|
|
272
|
+
this.pitchPanErrors = [];
|
|
273
|
+
this.page.on('pageerror', error => this.pitchPanErrors.push(error.message));
|
|
274
|
+
await this.page.evaluate(() => {
|
|
275
|
+
map.setView([50, 5], 5, { animate: false });
|
|
276
|
+
map.setBearing(45);
|
|
277
|
+
map.setPitch(70);
|
|
278
|
+
map.options.inertia = false;
|
|
279
|
+
const size = map.getSize(),
|
|
280
|
+
locations = [[0.35, 0.35], [0.65, 0.35], [0.35, 0.65], [0.65, 0.65]]
|
|
281
|
+
.map(([x, y]) => map.containerPointToLatLng([size.x * x, size.y * y])),
|
|
282
|
+
marker = new L.Marker(locations[0]).addTo(map),
|
|
283
|
+
vector = new L.CircleMarker(locations[0], { radius: 4 }).addTo(map),
|
|
284
|
+
bounds = element => {
|
|
285
|
+
const rect = element.getBoundingClientRect();
|
|
286
|
+
return [rect.x, rect.y, rect.width, rect.height];
|
|
287
|
+
};
|
|
288
|
+
window.__pitchRelease = {
|
|
289
|
+
locations,
|
|
290
|
+
resets: 0,
|
|
291
|
+
measure: () => ({
|
|
292
|
+
points: locations.map(ll => map.latLngToContainerPoint(ll)),
|
|
293
|
+
marker: bounds(marker.getElement()),
|
|
294
|
+
vector: bounds(vector.getElement()),
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
map.on('viewreset', () => window.__pitchRelease.resets++);
|
|
298
|
+
});
|
|
299
|
+
this.pitchReleases = [];
|
|
300
|
+
for (const [dx, dy] of [[0, -60], [-60, 0], [30, 50], [30, 10]]) {
|
|
301
|
+
const bounds = await this.page.locator('#map').boundingBox(),
|
|
302
|
+
x = bounds.x + bounds.width / 2,
|
|
303
|
+
y = bounds.y + bounds.height / 2;
|
|
304
|
+
await this.page.mouse.move(x, y);
|
|
305
|
+
await this.page.mouse.down();
|
|
306
|
+
await this.page.mouse.move(x + dx, y + dy, { steps: 10 });
|
|
307
|
+
const before = await this.page.evaluate(() => window.__pitchRelease.measure());
|
|
308
|
+
await this.page.mouse.up();
|
|
309
|
+
await this.page.waitForTimeout(50);
|
|
310
|
+
this.pitchReleases.push(await this.page.evaluate(before => {
|
|
311
|
+
const state = window.__pitchRelease,
|
|
312
|
+
after = state.measure(),
|
|
313
|
+
center = map.project(map.getCenter()),
|
|
314
|
+
size = map.getSize(),
|
|
315
|
+
bearing = map.getBearing() * Math.PI / 180,
|
|
316
|
+
pitch = map.getPitch() * Math.PI / 180,
|
|
317
|
+
perspective = size.y * 1.5,
|
|
318
|
+
projectionErrors = state.locations.map(ll => {
|
|
319
|
+
const point = map.project(ll).subtract(center),
|
|
320
|
+
x = point.x * Math.cos(bearing) - point.y * Math.sin(bearing),
|
|
321
|
+
y = point.x * Math.sin(bearing) + point.y * Math.cos(bearing),
|
|
322
|
+
scale = perspective / (perspective - y * Math.sin(pitch)),
|
|
323
|
+
actual = map.latLngToContainerPoint(ll);
|
|
324
|
+
return Math.hypot(actual.x - size.x / 2 - x * scale, actual.y - size.y / 2 - y * Math.cos(pitch) * scale);
|
|
325
|
+
});
|
|
326
|
+
return { before, after, resets: state.resets, projectionErrors };
|
|
327
|
+
}, before));
|
|
328
|
+
}
|
|
329
|
+
this.pitchPanToError = await this.page.evaluate(() => {
|
|
330
|
+
const center = map.getCenter(),
|
|
331
|
+
target = new L.LatLng(center.lat + 0.1, center.lng + 0.1);
|
|
332
|
+
map.panTo(target, { animate: false });
|
|
333
|
+
return map.latLngToContainerPoint(target).distanceTo(map.getSize().divideBy(2));
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
Then('releasing the map does not jump or reset its layers', function () {
|
|
338
|
+
assert.deepEqual(this.pitchPanErrors, [], 'Pitched dragging must not throw JavaScript errors');
|
|
339
|
+
assert.ok(this.pitchPanToError < 1.5, `panTo missed its target by ${this.pitchPanToError}px`);
|
|
340
|
+
for (const result of this.pitchReleases) {
|
|
341
|
+
assert.equal(result.resets, 0, 'Releasing a pan must not reset the view or reload its tiles');
|
|
342
|
+
for (let i = 0; i < result.before.points.length; i++) {
|
|
343
|
+
const before = result.before.points[i], after = result.after.points[i];
|
|
344
|
+
assert.ok(Math.hypot(before.x - after.x, before.y - after.y) < 0.1, JSON.stringify(result));
|
|
345
|
+
}
|
|
346
|
+
for (const layer of ['marker', 'vector']) {
|
|
347
|
+
assert.ok(result.before[layer].every((value, i) => Math.abs(value - result.after[layer][i]) < 0.1), JSON.stringify(result));
|
|
348
|
+
}
|
|
349
|
+
assert.ok(result.projectionErrors.every(error => error < 1.5), JSON.stringify(result));
|
|
222
350
|
}
|
|
223
351
|
});
|
|
224
352
|
|
package/test/support/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { After, AfterAll, Before, BeforeAll, Given, setDefaultTimeout } from '@cucumber/cucumber';
|
|
3
|
-
import { chromium } from 'playwright';
|
|
3
|
+
import { chromium, firefox } from 'playwright';
|
|
4
4
|
import { startServer } from '../../build/server.js';
|
|
5
5
|
|
|
6
6
|
setDefaultTimeout(10000);
|
|
@@ -9,19 +9,27 @@ let server;
|
|
|
9
9
|
let browser;
|
|
10
10
|
let baseUrl;
|
|
11
11
|
|
|
12
|
-
BeforeAll(async () => {
|
|
12
|
+
BeforeAll({ timeout: 60000 }, async () => {
|
|
13
13
|
server = await startServer(0);
|
|
14
14
|
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
15
|
-
|
|
15
|
+
const testBrowser = process.env.TEST_BROWSER;
|
|
16
|
+
const [engine, options] = testBrowser === 'firefox'
|
|
17
|
+
// Windows throttles requestAnimationFrame in windows it considers occluded.
|
|
18
|
+
? [firefox, { firefoxUserPrefs: { 'widget.windows.window_occlusion_tracking.enabled': false } }]
|
|
19
|
+
: [chromium, testBrowser === 'chrome' || testBrowser === 'msedge' ? { channel: testBrowser } : {}];
|
|
20
|
+
browser = await engine.launch(options);
|
|
16
21
|
});
|
|
17
22
|
|
|
18
23
|
AfterAll({ timeout: 30000 }, async () => {
|
|
19
24
|
await browser.close();
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
await new Promise((resolve, reject) => {
|
|
26
|
+
server.close(error => error ? reject(error) : resolve());
|
|
27
|
+
server.closeAllConnections();
|
|
28
|
+
});
|
|
22
29
|
});
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
// @unit scenarios run in Node only and need no browser page.
|
|
32
|
+
Before({ tags: 'not @unit' }, async function () {
|
|
25
33
|
this.baseUrl = baseUrl;
|
|
26
34
|
this.context = await browser.newContext();
|
|
27
35
|
await this.context.route(/.html$/, async route => {
|
|
@@ -37,6 +45,10 @@ Before(async function () {
|
|
|
37
45
|
'<script src="https://unpkg.com/leaflet@1.9/dist/leaflet-src.js"></script>',
|
|
38
46
|
'<script src="https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet-global-src.js"></script><script>globalThis.L = globalThis.leaflet;</script>'
|
|
39
47
|
)
|
|
48
|
+
.replace(
|
|
49
|
+
'"leaflet": "https://unpkg.com/leaflet@1.9/dist/leaflet-src.js"',
|
|
50
|
+
'"leaflet": "https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet-global-src.js"'
|
|
51
|
+
)
|
|
40
52
|
.replace('https://unpkg.com/leaflet@1.9/dist/leaflet.css', 'https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.css');
|
|
41
53
|
}
|
|
42
54
|
await route.fulfill({ response, body, headers: response.headers() });
|
|
@@ -44,7 +56,7 @@ Before(async function () {
|
|
|
44
56
|
this.page = await this.context.newPage();
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
After(async function () {
|
|
59
|
+
After({ tags: 'not @unit' }, async function () {
|
|
48
60
|
await this.context.close();
|
|
49
61
|
});
|
|
50
62
|
|