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.
@@ -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
- transform: map._rotatePane.style.transform,
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, transform } = this.pitchProjection;
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.match(transform, /rotateX\(45deg\)/);
109
- assert.ok(Math.abs(Number(transform.match(/perspective\(([\d.]+)px\)/)[1]) - size.y * 1.5) < 1);
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.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];
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, before, pinBefore } = window.__pitchPanTest,
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
- delete window.__pitchPanTest;
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
- 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],
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
- 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));
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
 
@@ -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
- browser = await chromium.launch();
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
- server.closeAllConnections();
21
- await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
25
+ await new Promise((resolve, reject) => {
26
+ server.close(error => error ? reject(error) : resolve());
27
+ server.closeAllConnections();
28
+ });
22
29
  });
23
30
 
24
- Before(async function () {
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