leaflet-rotate 0.5.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.
@@ -575,3 +575,196 @@ Then('its drag callbacks match the marker and pointer coordinates', function ()
575
575
  assert.ok(Math.abs(this.markerPosition.marker.lat - lastCallback[0]) < 1e-7, JSON.stringify(this.markerPosition));
576
576
  assert.ok(Math.abs(this.markerPosition.marker.lng - lastCallback[1]) < 1e-7, JSON.stringify(this.markerPosition));
577
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
- this.controlBearing = await this.page.evaluate(() => {
91
- map.setBearing(0);
92
- const link = map.rotateControl._link;
93
- link.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, clientX: 100, clientY: 100 }));
94
- document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: 135, clientY: 100 }));
95
- document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: 135, clientY: 100 }));
96
- return map.getBearing();
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
- Then('the map bearing follows the control drag', function () {
101
- assert.equal(this.controlBearing, 35);
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
- When('I activate compass mode on a permission-based device', async function () {
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
- const click = () => map.rotateControl._link.dispatchEvent(new MouseEvent('click', {
118
- bubbles: true,
119
- cancelable: true,
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.equal(this.permissionRequests, 1);
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 = JSON.parse(document.getElementById('ogbx').textContent);
331
- const debugMax = JSON.parse(document.getElementById('ogby').textContent);
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.x - pixelBounds.min.x,
347
- debugMin.y - pixelBounds.min.y,
348
- debugMax.x - pixelBounds.max.x,
349
- debugMax.y - pixelBounds.max.y,
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 Markers' }).click();
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;