leaflet-rotate 0.2.7 → 0.3.0
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/.github/workflows/ci.yml +21 -0
- package/README.md +5 -1
- package/build/build.js +69 -0
- package/build/dev.js +24 -0
- package/build/server.js +104 -0
- package/cucumber.mjs +5 -0
- package/dist/leaflet-rotate-src.js +1380 -1853
- package/dist/leaflet-rotate-src.js.map +7 -1
- package/dist/leaflet-rotate.js +1 -1
- package/dist/leaflet-rotate.js.map +7 -1
- package/lib/debug.js +19 -8
- package/lib/debug.min.js +1 -1
- package/misc/leaflet-heat-rotate.js +96 -0
- package/package.json +7 -14
- package/src/control/Rotate.js +5 -1
- package/src/layer/DivOverlay.js +13 -1
- package/src/layer/marker/Marker.js +16 -2
- package/src/layer/vector/Renderer.js +4 -0
- package/src/map/Map.js +62 -3
- package/src/map/handler/CompassBearing.js +5 -3
- package/src/map/handler/TouchGestures.js +8 -0
- package/test/features/example.feature +87 -0
- package/test/features/gestures.feature +33 -0
- package/test/features/overlays.feature +58 -0
- package/test/features/rotation.feature +36 -0
- package/test/steps/bearing.steps.js +235 -0
- package/test/steps/example.steps.js +363 -0
- package/test/steps/gestures.steps.js +145 -0
- package/test/steps/overlays.steps.js +427 -0
- package/test/support/hooks.js +44 -0
- package/build/rollup.config.js +0 -34
- package/test/setup/http_server.js +0 -86
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { Then, When } from '@cucumber/cucumber';
|
|
3
|
+
|
|
4
|
+
Then('the zoom transition ends before the gesture ends', function () {
|
|
5
|
+
assert.ok(this.zoomEndsDuringGesture > 0);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
Then('the vector path remains aligned during the gesture', function () {
|
|
9
|
+
assert.ok(this.gesturePathPositions.bearingChange > 1, JSON.stringify(this.gesturePathPositions));
|
|
10
|
+
assert.ok(this.gesturePathPositions.zoomChange > 0, JSON.stringify(this.gesturePathPositions));
|
|
11
|
+
assert.ok(Math.hypot(...this.gesturePathPositions.delta) < 2, JSON.stringify(this.gesturePathPositions));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
Then('the map bearing remains unchanged', function () {
|
|
15
|
+
assert.equal(this.bearings[1], this.bearings[0]);
|
|
16
|
+
assert.equal(this.bearings[2], false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
When('I move a two-finger pinch gesture', async function () {
|
|
20
|
+
this.zoomEndsDuringGesture = await this.page.evaluate(() => {
|
|
21
|
+
const container = map.getContainer();
|
|
22
|
+
const gestures = map.touchGestures;
|
|
23
|
+
const wasZoomEnabled = gestures.zoom;
|
|
24
|
+
const rect = container.getBoundingClientRect();
|
|
25
|
+
const centerX = rect.left + rect.width / 2;
|
|
26
|
+
const centerY = rect.top + rect.height / 2;
|
|
27
|
+
const touches = (offsetX, offsetY) => [
|
|
28
|
+
new Touch({ identifier: 1, target: container, clientX: centerX - offsetX, clientY: centerY - offsetY }),
|
|
29
|
+
new Touch({ identifier: 2, target: container, clientX: centerX + offsetX, clientY: centerY + offsetY }),
|
|
30
|
+
];
|
|
31
|
+
const dispatch = (target, type, activeTouches, changedTouches = activeTouches) => {
|
|
32
|
+
target.dispatchEvent(new TouchEvent(type, {
|
|
33
|
+
bubbles: true,
|
|
34
|
+
cancelable: true,
|
|
35
|
+
touches: activeTouches,
|
|
36
|
+
targetTouches: activeTouches,
|
|
37
|
+
changedTouches,
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
let zoomEnds = 0;
|
|
41
|
+
const onZoomEnd = () => zoomEnds++;
|
|
42
|
+
|
|
43
|
+
gestures.zoom = true;
|
|
44
|
+
map.on('zoomend', onZoomEnd);
|
|
45
|
+
const startTouches = touches(30, 0);
|
|
46
|
+
dispatch(container, 'touchstart', startTouches);
|
|
47
|
+
dispatch(document, 'touchmove', touches(40, 30));
|
|
48
|
+
const result = zoomEnds;
|
|
49
|
+
dispatch(document, 'touchend', [], touches(40, 30));
|
|
50
|
+
map.off('zoomend', onZoomEnd);
|
|
51
|
+
gestures.zoom = wasZoomEnabled;
|
|
52
|
+
return result;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
When('I drag a constrained marker on a rotated map', async function () {
|
|
57
|
+
await this.page.evaluate(() => {
|
|
58
|
+
map.options.zoomAnimation = false;
|
|
59
|
+
map.setMaxBounds([[-89.98155760646617, -180], [89.99346179538875, 180]]);
|
|
60
|
+
map.setView([23.1799, 113.4103], 15, { animate: false });
|
|
61
|
+
map.setBearing(30);
|
|
62
|
+
map.setZoom(16, { animate: false });
|
|
63
|
+
window.dragMarker = L.marker([23.1799, 113.4103], { draggable: true }).addTo(map);
|
|
64
|
+
window.dragMarkerStart = window.dragMarker.getLatLng();
|
|
65
|
+
window.dragCallbackLatLngs = [];
|
|
66
|
+
window.dragMarker.on('drag dragend', event => {
|
|
67
|
+
window.dragCallbackLatLngs.push([event.latlng.lat, event.latlng.lng]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const box = await this.page.locator('.leaflet-marker-icon').last().boundingBox();
|
|
72
|
+
const start = { x: box.x + box.width / 2, y: box.y + box.height - 5 };
|
|
73
|
+
const end = { x: start.x + 80, y: start.y + 40 };
|
|
74
|
+
const markerOffset = await this.page.evaluate(({ x, y }) => {
|
|
75
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
76
|
+
const markerPoint = map.latLngToContainerPoint(window.dragMarker.getLatLng());
|
|
77
|
+
return [x - rect.left - markerPoint.x, y - rect.top - markerPoint.y];
|
|
78
|
+
}, start);
|
|
79
|
+
await this.page.mouse.move(start.x, start.y);
|
|
80
|
+
await this.page.mouse.down();
|
|
81
|
+
await this.page.mouse.move(end.x, end.y, { steps: 8 });
|
|
82
|
+
await this.page.mouse.up();
|
|
83
|
+
|
|
84
|
+
this.markerPosition = await this.page.evaluate(({ x, y, markerOffset }) => {
|
|
85
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
86
|
+
const pointerPosition = map.containerPointToLatLng([x - rect.left, y - rect.top]);
|
|
87
|
+
const markerPosition = map.latLngToContainerPoint(window.dragMarker.getLatLng());
|
|
88
|
+
const expected = [x - rect.left - markerOffset[0], y - rect.top - markerOffset[1]];
|
|
89
|
+
return {
|
|
90
|
+
delta: [markerPosition.x - expected[0], markerPosition.y - expected[1]],
|
|
91
|
+
moved: !window.dragMarkerStart.equals(window.dragMarker.getLatLng()),
|
|
92
|
+
marker: window.dragMarker.getLatLng(),
|
|
93
|
+
pointer: pointerPosition,
|
|
94
|
+
callbacks: window.dragCallbackLatLngs,
|
|
95
|
+
};
|
|
96
|
+
}, { ...end, markerOffset });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
When('I drag a map with interactive layers while rotated', async function () {
|
|
100
|
+
await this.page.evaluate(() => {
|
|
101
|
+
map.options.zoomAnimation = false;
|
|
102
|
+
map.setView([55, 10], 6, { animate: false });
|
|
103
|
+
const marker = L.marker([55.1, 10.1]).addTo(map);
|
|
104
|
+
const path = L.circleMarker([55.2, 10.2], { radius: 8 }).addTo(map);
|
|
105
|
+
map.setBearing(35);
|
|
106
|
+
window.interactiveLayers = { marker, path };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const bounds = await this.page.locator('#map').boundingBox();
|
|
110
|
+
const start = { x: bounds.x + bounds.width * 0.8, y: bounds.y + bounds.height * 0.5 };
|
|
111
|
+
const end = { x: start.x - 75, y: start.y + 45 };
|
|
112
|
+
await this.page.mouse.move(start.x, start.y);
|
|
113
|
+
await this.page.mouse.down();
|
|
114
|
+
await this.page.mouse.move(end.x, end.y, { steps: 8 });
|
|
115
|
+
await this.page.mouse.up();
|
|
116
|
+
|
|
117
|
+
this.interactivePositions = await this.page.evaluate(() => {
|
|
118
|
+
const { marker, path } = window.interactiveLayers;
|
|
119
|
+
const measure = () => {
|
|
120
|
+
const markerElement = marker.getElement();
|
|
121
|
+
const markerRect = markerElement.getBoundingClientRect();
|
|
122
|
+
const mapRect = map.getContainer().getBoundingClientRect();
|
|
123
|
+
const markerScreen = [markerRect.left + markerRect.width / 2 - mapRect.left, markerRect.bottom - mapRect.top];
|
|
124
|
+
const markerExpected = map.latLngToContainerPoint(marker.getLatLng());
|
|
125
|
+
const pathElement = path.getElement();
|
|
126
|
+
const pathBounds = pathElement.getBBox();
|
|
127
|
+
const pathScreen = new DOMPoint(pathBounds.x + pathBounds.width / 2, pathBounds.y + pathBounds.height / 2)
|
|
128
|
+
.matrixTransform(pathElement.getScreenCTM());
|
|
129
|
+
const pathExpected = map.latLngToContainerPoint(path.getLatLng());
|
|
130
|
+
return [
|
|
131
|
+
markerScreen[0] - markerExpected.x,
|
|
132
|
+
markerScreen[1] - markerExpected.y,
|
|
133
|
+
pathScreen.x - mapRect.left - pathExpected.x,
|
|
134
|
+
pathScreen.y - mapRect.top - pathExpected.y,
|
|
135
|
+
];
|
|
136
|
+
};
|
|
137
|
+
const moved = !map.getCenter().equals(L.latLng(55, 10));
|
|
138
|
+
const positions = [measure()];
|
|
139
|
+
map.setBearing(0);
|
|
140
|
+
positions.push(measure());
|
|
141
|
+
map.setBearing(35);
|
|
142
|
+
positions.push(measure());
|
|
143
|
+
return { positions, moved };
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { Then, When } from '@cucumber/cucumber';
|
|
3
|
+
|
|
4
|
+
Then('the label remains upright while following the map bearing', function () {
|
|
5
|
+
const normalize = angle => ((angle % 360) + 360) % 360;
|
|
6
|
+
for (const { labelAngle, roadAngle } of this.labelAngles) {
|
|
7
|
+
assert.ok(labelAngle <= 90 || labelAngle >= 270, JSON.stringify(this.labelAngles));
|
|
8
|
+
assert.ok(Math.abs(normalize(labelAngle - roadAngle + 90) % 180 - 90) < 1,
|
|
9
|
+
JSON.stringify(this.labelAngles));
|
|
10
|
+
}
|
|
11
|
+
assert.deepEqual(this.labelAngles.map(({ labelAngle }) => Math.round(labelAngle)), [315, 0, 45, 315, 45]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
Then('the custom-pane GeoJSON stays aligned with its coordinates', function () {
|
|
15
|
+
assert.equal(this.customPanePosition.parentIsRotated, true);
|
|
16
|
+
assert.ok(Math.hypot(...this.customPanePosition.delta) < 2, JSON.stringify(this.customPanePosition));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
Then('the Canvas renderer belongs to the rotating pane', function () {
|
|
20
|
+
assert.equal(this.canvasPane.rotating, true);
|
|
21
|
+
assert.equal(this.canvasPane.bearing, 35);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Then('the map rotates without invoking the removed control', function () {
|
|
25
|
+
assert.equal(this.removedControl.bearing, 40);
|
|
26
|
+
assert.equal(this.removedControl.attached, false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
Then('only the requested marker is draggable', function () {
|
|
30
|
+
assert.deepEqual(this.markerDragStates, [false, true]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
Then('the click event reports the matching geographic coordinate', function () {
|
|
34
|
+
assert.ok(this.mapClick.containerPoint.every((value, index) => Math.abs(value - 100) <= 1));
|
|
35
|
+
assert.ok(Math.abs(this.mapClick.latlng.lat - this.mapClick.expected.lat) < 1e-7, JSON.stringify(this.mapClick));
|
|
36
|
+
assert.ok(Math.abs(this.mapClick.latlng.lng - this.mapClick.expected.lng) < 1e-7, JSON.stringify(this.mapClick));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
Then('the heat points remain aligned with their coordinates', function () {
|
|
40
|
+
assert.equal(this.heatPointDeltas.length, 24);
|
|
41
|
+
for (const { delta } of this.heatPointDeltas) {
|
|
42
|
+
assert.ok(Math.hypot(...delta) < 1, JSON.stringify(this.heatPointDeltas));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Then('the map center remains unchanged', function () {
|
|
47
|
+
assert.equal(this.centerUnchanged, true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
Then('the path stays at its geographic coordinate', function () {
|
|
51
|
+
for (const position of Object.values(this.pathPosition)) {
|
|
52
|
+
assert.ok(Math.abs(position[0]) < 2, JSON.stringify(this.pathPosition));
|
|
53
|
+
assert.ok(Math.abs(position[1]) < 2, JSON.stringify(this.pathPosition));
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
Then('the marker and vector path stay at their geographic coordinates', function () {
|
|
58
|
+
assert.equal(this.interactivePositions.moved, true, JSON.stringify(this.interactivePositions));
|
|
59
|
+
for (const position of this.interactivePositions.positions) {
|
|
60
|
+
assert.ok(Math.hypot(...position) < 3, JSON.stringify(this.interactivePositions));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
When('I rotate a label marker with the view', async function () {
|
|
65
|
+
this.labelAngles = await this.page.evaluate(() => {
|
|
66
|
+
map.setView([55, 10], 6, { animate: false });
|
|
67
|
+
map.setBearing(0);
|
|
68
|
+
const label = L.marker([55, 10], {
|
|
69
|
+
icon: L.divIcon({ className: 'road-label', html: 'Road name' }),
|
|
70
|
+
rotation: 135 * L.DomUtil.DEG_TO_RAD,
|
|
71
|
+
rotateWithView: true,
|
|
72
|
+
keepUpright: true,
|
|
73
|
+
}).addTo(map);
|
|
74
|
+
const center = map.latLngToContainerPoint(label.getLatLng());
|
|
75
|
+
const direction = 135 * L.DomUtil.DEG_TO_RAD;
|
|
76
|
+
const road = L.polyline([
|
|
77
|
+
map.containerPointToLatLng(center.subtract([20 * Math.cos(direction), 20 * Math.sin(direction)])),
|
|
78
|
+
map.containerPointToLatLng(center.add([20 * Math.cos(direction), 20 * Math.sin(direction)])),
|
|
79
|
+
]).addTo(map);
|
|
80
|
+
return [0, 45, 90, 180, 270].map(bearing => {
|
|
81
|
+
map.setBearing(bearing);
|
|
82
|
+
const transform = new DOMMatrix(getComputedStyle(label.getElement()).transform);
|
|
83
|
+
const labelAngle = (Math.atan2(transform.b, transform.a) * L.DomUtil.RAD_TO_DEG + 360) % 360;
|
|
84
|
+
const endpoints = road.getLatLngs().map(latlng => map.latLngToContainerPoint(latlng));
|
|
85
|
+
const roadAngle = (Math.atan2(
|
|
86
|
+
endpoints[1].y - endpoints[0].y,
|
|
87
|
+
endpoints[1].x - endpoints[0].x,
|
|
88
|
+
) * L.DomUtil.RAD_TO_DEG + 360) % 360;
|
|
89
|
+
return { labelAngle, roadAngle };
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
When('I add GeoJSON to a custom pane inside the rotate pane', async function () {
|
|
95
|
+
this.customPanePosition = await this.page.evaluate(() => {
|
|
96
|
+
map.setView([55, 10], 6, { animate: false });
|
|
97
|
+
map.setBearing(35);
|
|
98
|
+
const pane = map.createPane('customGeoJsonPane', map.getPane('rotatePane'));
|
|
99
|
+
const geoJson = L.geoJSON({
|
|
100
|
+
type: 'Feature',
|
|
101
|
+
geometry: { type: 'Point', coordinates: [10, 55] },
|
|
102
|
+
properties: {},
|
|
103
|
+
}, {
|
|
104
|
+
pane: 'customGeoJsonPane',
|
|
105
|
+
pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 8 }),
|
|
106
|
+
}).addTo(map);
|
|
107
|
+
const element = geoJson.getLayers()[0].getElement();
|
|
108
|
+
const bounds = element.getBBox();
|
|
109
|
+
const screen = new DOMPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)
|
|
110
|
+
.matrixTransform(element.getScreenCTM());
|
|
111
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
112
|
+
const expected = map.latLngToContainerPoint([55, 10]);
|
|
113
|
+
return {
|
|
114
|
+
parentIsRotated: pane.parentNode === map.getPane('rotatePane'),
|
|
115
|
+
delta: [screen.x - rect.left - expected.x, screen.y - rect.top - expected.y],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
When('I render a Canvas vector layer on a rotated map', async function () {
|
|
121
|
+
this.canvasPane = await this.page.evaluate(() => {
|
|
122
|
+
map.setBearing(35);
|
|
123
|
+
const renderer = L.canvas({ pane: 'overlayPane' });
|
|
124
|
+
L.circleMarker([55, 10], { renderer, radius: 8 }).addTo(map);
|
|
125
|
+
return {
|
|
126
|
+
rotating: renderer._container.parentNode === map.getPane('overlayPane')
|
|
127
|
+
&& map.getPane('overlayPane').parentNode === map.getPane('rotatePane'),
|
|
128
|
+
bearing: map.getBearing(),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
When('I remove the rotate control and change the map bearing', async function () {
|
|
134
|
+
this.removedControl = await this.page.evaluate(() => {
|
|
135
|
+
const control = L.control.rotate({ closeOnZeroBearing: false }).addTo(map);
|
|
136
|
+
map.removeControl(control);
|
|
137
|
+
map.setBearing(40);
|
|
138
|
+
return { bearing: map.getBearing(), attached: Boolean(control._map) };
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
When('I create default and draggable markers', async function () {
|
|
143
|
+
this.markerDragStates = await this.page.evaluate(() => {
|
|
144
|
+
const defaultMarker = L.marker([55, 10]).addTo(map);
|
|
145
|
+
const draggableMarker = L.marker([55.1, 10.1], { draggable: true }).addTo(map);
|
|
146
|
+
return [defaultMarker, draggableMarker].map(marker => marker.dragging.enabled());
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
When('I click a rotated map at a known container point', async function () {
|
|
151
|
+
this.mapClick = await this.page.evaluate(() => new Promise(resolve => {
|
|
152
|
+
map.setView([55, 10], 6, { animate: false });
|
|
153
|
+
map.setBearing(35);
|
|
154
|
+
const point = [100, 100];
|
|
155
|
+
map.once('click', event => resolve({
|
|
156
|
+
containerPoint: [event.containerPoint.x, event.containerPoint.y],
|
|
157
|
+
latlng: event.latlng,
|
|
158
|
+
expected: map.containerPointToLatLng(event.containerPoint),
|
|
159
|
+
}));
|
|
160
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
161
|
+
map.getContainer().dispatchEvent(new MouseEvent('click', {
|
|
162
|
+
bubbles: true,
|
|
163
|
+
clientX: rect.left + point[0],
|
|
164
|
+
clientY: rect.top + point[1],
|
|
165
|
+
}));
|
|
166
|
+
}));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
When('I focus a marker near the edge of a rotated map', async function () {
|
|
170
|
+
this.centerUnchanged = await this.page.evaluate(() => {
|
|
171
|
+
map.setView([55, 10], 6, { animate: false });
|
|
172
|
+
map.setBearing(30);
|
|
173
|
+
const point = map.containerPointToLatLng([35, map.getSize().y / 2]);
|
|
174
|
+
const marker = L.marker(point, { autoPanOnFocus: true }).addTo(map);
|
|
175
|
+
const before = map.getCenter();
|
|
176
|
+
marker.getElement().focus();
|
|
177
|
+
return before.equals(map.getCenter());
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
When('I redraw a heat layer after rotating, panning, and zooming', async function () {
|
|
182
|
+
await this.page.evaluate(() => {
|
|
183
|
+
map.setView([55, 10], 6, { animate: false });
|
|
184
|
+
map.setBearing(35);
|
|
185
|
+
L.HeatLayer = L.Layer.extend({
|
|
186
|
+
initialize: function(latlngs) {
|
|
187
|
+
this._latlngs = latlngs;
|
|
188
|
+
},
|
|
189
|
+
onAdd: function(map) {
|
|
190
|
+
this._map = map;
|
|
191
|
+
const size = map.getSize();
|
|
192
|
+
this._heat = { _width: size.x, _height: size.y };
|
|
193
|
+
this._canvas = L.DomUtil.create('canvas', 'leaflet-heatmap-layer');
|
|
194
|
+
this._canvas.width = size.x;
|
|
195
|
+
this._canvas.height = size.y;
|
|
196
|
+
map.getPane('overlayPane').appendChild(this._canvas);
|
|
197
|
+
L.DomUtil.setPosition(this._canvas, map.containerPointToLayerPoint([0, 0]));
|
|
198
|
+
map.on('moveend', this._reset, this);
|
|
199
|
+
this._reset();
|
|
200
|
+
},
|
|
201
|
+
onRemove: function(map) {
|
|
202
|
+
map.off('moveend', this._reset, this);
|
|
203
|
+
L.DomUtil.remove(this._canvas);
|
|
204
|
+
},
|
|
205
|
+
_redraw: function() {
|
|
206
|
+
this._drawnPoints = this._latlngs.map(latlng => this._map.latLngToContainerPoint(latlng));
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
L.heatLayer = latlngs => new L.HeatLayer(latlngs);
|
|
210
|
+
globalThis.HeatmapOverlay = L.Layer.extend({
|
|
211
|
+
initialize: function(latlngs) {
|
|
212
|
+
this._latlngs = latlngs;
|
|
213
|
+
this._el = L.DomUtil.create('div', 'leaflet-heatmap-overlay');
|
|
214
|
+
},
|
|
215
|
+
onAdd: function(map) {
|
|
216
|
+
this._map = map;
|
|
217
|
+
map.getPane('overlayPane').appendChild(this._el);
|
|
218
|
+
map.on('moveend', this._draw, this);
|
|
219
|
+
this._draw();
|
|
220
|
+
},
|
|
221
|
+
onRemove: function(map) {
|
|
222
|
+
map.off('moveend', this._draw, this);
|
|
223
|
+
L.DomUtil.remove(this._el);
|
|
224
|
+
},
|
|
225
|
+
_draw: function() {
|
|
226
|
+
this._update();
|
|
227
|
+
},
|
|
228
|
+
_update: function() {
|
|
229
|
+
this._drawnPoints = this._latlngs.map(latlng => this._map.latLngToContainerPoint(latlng));
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
await this.page.addScriptTag({ url: `${this.baseUrl}/misc/leaflet-heat-rotate.js` });
|
|
234
|
+
this.heatPointDeltas = await this.page.evaluate(async () => {
|
|
235
|
+
const locations = [L.latLng(55.1, 10.1), L.latLng(54.9, 9.8), L.latLng(55.25, 10.3)];
|
|
236
|
+
const layer = L.heatLayer(locations).addTo(map);
|
|
237
|
+
const overlay = new HeatmapOverlay(locations).addTo(map);
|
|
238
|
+
const deltas = [];
|
|
239
|
+
const measure = heatLayer => {
|
|
240
|
+
const origin = L.DomUtil.getPosition(heatLayer._canvas || heatLayer._el);
|
|
241
|
+
const panePosition = map._getMapPanePos();
|
|
242
|
+
for (let index = 0; index < locations.length; index++) {
|
|
243
|
+
const actual = origin.add(heatLayer._drawnPoints[index]).add(panePosition);
|
|
244
|
+
const expected = map.latLngToContainerPoint(locations[index]);
|
|
245
|
+
deltas.push({
|
|
246
|
+
delta: [actual.x - expected.x, actual.y - expected.y],
|
|
247
|
+
origin: origin.toString(),
|
|
248
|
+
pane: panePosition.toString(),
|
|
249
|
+
point: heatLayer._drawnPoints[index].toString(),
|
|
250
|
+
expected: expected.toString(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const measureLayers = () => {
|
|
255
|
+
measure(layer);
|
|
256
|
+
measure(overlay);
|
|
257
|
+
if (layer._canvas.parentNode !== map.getPane('norotatePane')
|
|
258
|
+
|| overlay._el.parentNode !== map.getPane('norotatePane')) {
|
|
259
|
+
throw new Error('Heat layers must be rendered in the non-rotating pane');
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
measureLayers();
|
|
263
|
+
map.panBy([45, 25], { animate: false });
|
|
264
|
+
measureLayers();
|
|
265
|
+
await new Promise(resolve => {
|
|
266
|
+
map.once('zoomend', resolve);
|
|
267
|
+
map.setZoom(7);
|
|
268
|
+
});
|
|
269
|
+
measureLayers();
|
|
270
|
+
map.setBearing(0);
|
|
271
|
+
map.panBy([-25, 35], { animate: false });
|
|
272
|
+
measureLayers();
|
|
273
|
+
return deltas;
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
When('I focus a point near the left edge of an offset map', async function () {
|
|
278
|
+
this.centerUnchanged = await this.page.evaluate(() => {
|
|
279
|
+
const container = map.getContainer();
|
|
280
|
+
Object.assign(container.style, { width: '400px', height: '300px', marginLeft: '100px' });
|
|
281
|
+
map.invalidateSize({ animate: false });
|
|
282
|
+
map.setBearing(0);
|
|
283
|
+
const before = map.getCenter();
|
|
284
|
+
const point = map.containerPointToLatLng([5, map.getSize().y / 2]);
|
|
285
|
+
map.panInside(point, { animate: false });
|
|
286
|
+
return before.equals(map.getCenter());
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
When('I zoom an SVG path near the latitude limit', async function () {
|
|
291
|
+
this.pathPosition = await this.page.evaluate(() => {
|
|
292
|
+
map.options.zoomAnimation = false;
|
|
293
|
+
map.setView([85, 0], 5, { animate: false });
|
|
294
|
+
map.setBearing(30);
|
|
295
|
+
const location = L.latLng(85, 0);
|
|
296
|
+
const path = L.circleMarker(location, { radius: 6 }).addTo(map);
|
|
297
|
+
const pathPoint = () => {
|
|
298
|
+
const element = path.getElement();
|
|
299
|
+
const bounds = element.getBBox();
|
|
300
|
+
const actual = new DOMPoint(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)
|
|
301
|
+
.matrixTransform(element.getScreenCTM());
|
|
302
|
+
const expected = map.latLngToContainerPoint(location);
|
|
303
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
304
|
+
return [actual.x - rect.left - expected.x, actual.y - rect.top - expected.y];
|
|
305
|
+
};
|
|
306
|
+
const initial = pathPoint();
|
|
307
|
+
map.setZoom(7, { animate: false });
|
|
308
|
+
const zoomed = pathPoint();
|
|
309
|
+
map.setZoom(4, { animate: false });
|
|
310
|
+
return { initial, zoomed, final: pathPoint() };
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
When('I open a popup on a rotated map without zoom animation', async function () {
|
|
315
|
+
this.popupPosition = await this.page.evaluate(() => {
|
|
316
|
+
map.options.zoomAnimation = false;
|
|
317
|
+
map.setView([55, 10], 6, { animate: false });
|
|
318
|
+
map.setBearing(30);
|
|
319
|
+
const marker = L.marker([55, 10]).addTo(map).bindPopup('popup');
|
|
320
|
+
marker.getElement().click();
|
|
321
|
+
const popup = marker.getPopup();
|
|
322
|
+
const tip = popup.getElement().querySelector('.leaflet-popup-tip-container');
|
|
323
|
+
const tipRect = tip.getBoundingClientRect();
|
|
324
|
+
const mapRect = map.getContainer().getBoundingClientRect();
|
|
325
|
+
const actual = [tipRect.left + tipRect.width / 2 - mapRect.left, tipRect.bottom - mapRect.top];
|
|
326
|
+
const expected = map.latLngToContainerPoint(marker.getLatLng())
|
|
327
|
+
.add(popup._getAnchor()).add(popup.options.offset);
|
|
328
|
+
return {
|
|
329
|
+
delta: [actual[0] - expected.x, actual[1] - expected.y],
|
|
330
|
+
bearing: map.getBearing(),
|
|
331
|
+
actual,
|
|
332
|
+
expected,
|
|
333
|
+
anchor: popup._getAnchor(),
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
Then('the popup tip stays at the marker coordinate', function () {
|
|
339
|
+
assert.ok(Math.abs(this.popupPosition.bearing - 30) < 1e-6);
|
|
340
|
+
assert.ok(Math.hypot(...this.popupPosition.delta) < 2, JSON.stringify(this.popupPosition));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
When('I open a popup near the edge of a rotated map', async function () {
|
|
344
|
+
this.edgePopup = await this.page.evaluate(async () => {
|
|
345
|
+
map.options.zoomAnimation = false;
|
|
346
|
+
map.setView([55, 10], 6, { animate: false });
|
|
347
|
+
map.setBearing(30);
|
|
348
|
+
map.on('move zoomend dragend locationfound locationerror', () => {});
|
|
349
|
+
const edge = map.containerPointToLatLng([map.getSize().x - 10, map.getSize().y / 2]);
|
|
350
|
+
const marker = L.marker(edge).addTo(map).bindPopup('edge popup');
|
|
351
|
+
const oldCenter = map.project(map.getCenter(), map.getZoom());
|
|
352
|
+
marker.openPopup();
|
|
353
|
+
await new Promise(resolve => {
|
|
354
|
+
if (!map._panAnim || !map._panAnim._inProgress) return resolve();
|
|
355
|
+
map.once('moveend', resolve);
|
|
356
|
+
});
|
|
357
|
+
const rect = map.getContainer().getBoundingClientRect();
|
|
358
|
+
const popupRect = marker.getPopup().getElement().getBoundingClientRect();
|
|
359
|
+
const newCenter = map.project(map.getCenter(), map.getZoom());
|
|
360
|
+
return {
|
|
361
|
+
inside: popupRect.left >= rect.left - 1 && popupRect.right <= rect.right + 1
|
|
362
|
+
&& popupRect.top >= rect.top - 1 && popupRect.bottom <= rect.bottom + 1,
|
|
363
|
+
movement: oldCenter.distanceTo(newCenter),
|
|
364
|
+
width: map.getSize().x,
|
|
365
|
+
height: map.getSize().y,
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
Then('the popup stays inside the map without a large jump', function () {
|
|
371
|
+
assert.equal(this.edgePopup.inside, true, JSON.stringify(this.edgePopup));
|
|
372
|
+
assert.ok(this.edgePopup.movement > 1, JSON.stringify(this.edgePopup));
|
|
373
|
+
assert.ok(this.edgePopup.movement < Math.max(this.edgePopup.width, this.edgePopup.height) / 2,
|
|
374
|
+
JSON.stringify(this.edgePopup));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
When('I open a popup in a second rotated map with map listeners', async function () {
|
|
378
|
+
this.secondaryPopup = await this.page.evaluate(() => {
|
|
379
|
+
const element = document.createElement('div');
|
|
380
|
+
element.id = 'secondary-map';
|
|
381
|
+
element.style.cssText = 'height: 300px; width: 500px; margin-top: 40px';
|
|
382
|
+
document.body.append(element);
|
|
383
|
+
const secondMap = window.secondaryMap = L.map(element, {
|
|
384
|
+
center: [55, 10], zoom: 6, rotate: true, bearing: 30,
|
|
385
|
+
});
|
|
386
|
+
const marker = L.marker([55.5, 10.5]).addTo(secondMap)
|
|
387
|
+
.bindPopup('popup')
|
|
388
|
+
.bindTooltip('tooltip', { permanent: true, direction: 'right' });
|
|
389
|
+
marker.openTooltip();
|
|
390
|
+
secondMap.on('move zoomend dragend locationfound locationerror', () => {});
|
|
391
|
+
const center = secondMap.getCenter();
|
|
392
|
+
const top = secondMap.getContainer().getBoundingClientRect().top;
|
|
393
|
+
marker.getElement().click();
|
|
394
|
+
const popup = marker.getPopup();
|
|
395
|
+
const tip = popup.getElement().querySelector('.leaflet-popup-tip-container');
|
|
396
|
+
const tipRect = tip.getBoundingClientRect();
|
|
397
|
+
const mapRect = secondMap.getContainer().getBoundingClientRect();
|
|
398
|
+
const actual = [tipRect.left + tipRect.width / 2 - mapRect.left, tipRect.bottom - mapRect.top];
|
|
399
|
+
const expected = secondMap.latLngToContainerPoint(marker.getLatLng())
|
|
400
|
+
.add(popup._getAnchor()).add(popup.options.offset);
|
|
401
|
+
const tooltip = marker.getTooltip();
|
|
402
|
+
const tooltipRect = tooltip.getElement().getBoundingClientRect();
|
|
403
|
+
const tooltipOffset = L.point(tooltip.options.offset);
|
|
404
|
+
const tooltipPoint = secondMap.latLngToContainerPoint(marker.getLatLng()).add(tooltip._getAnchor()).add(tooltipOffset);
|
|
405
|
+
const tooltipExpected = [
|
|
406
|
+
tooltipPoint.x + parseFloat(getComputedStyle(tooltip.getElement()).marginLeft),
|
|
407
|
+
tooltipPoint.y,
|
|
408
|
+
];
|
|
409
|
+
const tooltipActual = [
|
|
410
|
+
tooltipRect.left - mapRect.left,
|
|
411
|
+
tooltipRect.top + tooltipRect.height / 2 - mapRect.top,
|
|
412
|
+
];
|
|
413
|
+
return {
|
|
414
|
+
centerUnchanged: center.equals(secondMap.getCenter()),
|
|
415
|
+
delta: [actual[0] - expected.x, actual[1] - expected.y],
|
|
416
|
+
tooltipDelta: [tooltipActual[0] - tooltipExpected[0], tooltipActual[1] - tooltipExpected[1]],
|
|
417
|
+
mapTopDelta: mapRect.top - top,
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
Then('the second map center remains unchanged and its popup stays anchored', function () {
|
|
423
|
+
assert.equal(this.secondaryPopup.centerUnchanged, true, JSON.stringify(this.secondaryPopup));
|
|
424
|
+
assert.equal(this.secondaryPopup.mapTopDelta, 0, JSON.stringify(this.secondaryPopup));
|
|
425
|
+
assert.ok(Math.hypot(...this.secondaryPopup.delta) < 2, JSON.stringify(this.secondaryPopup));
|
|
426
|
+
assert.ok(Math.hypot(...this.secondaryPopup.tooltipDelta) < 2, JSON.stringify(this.secondaryPopup));
|
|
427
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { After, AfterAll, Before, BeforeAll, Given, setDefaultTimeout } from '@cucumber/cucumber';
|
|
3
|
+
import { chromium } from 'playwright';
|
|
4
|
+
import { startServer } from '../../build/server.js';
|
|
5
|
+
|
|
6
|
+
setDefaultTimeout(10000);
|
|
7
|
+
|
|
8
|
+
let server;
|
|
9
|
+
let browser;
|
|
10
|
+
let baseUrl;
|
|
11
|
+
|
|
12
|
+
BeforeAll(async () => {
|
|
13
|
+
server = await startServer(0);
|
|
14
|
+
baseUrl = `http://127.0.0.1:${server.address().port}`;
|
|
15
|
+
browser = await chromium.launch();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
AfterAll({ timeout: 30000 }, async () => {
|
|
19
|
+
await browser.close();
|
|
20
|
+
server.closeAllConnections();
|
|
21
|
+
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Before(async function () {
|
|
25
|
+
this.baseUrl = baseUrl;
|
|
26
|
+
this.context = await browser.newContext();
|
|
27
|
+
await this.context.route(/.html$/, async route => {
|
|
28
|
+
const response = await route.fetch();
|
|
29
|
+
const body = (await response.text())
|
|
30
|
+
.replace(/https:\/\/unpkg\.com\/leaflet-rotate@(.*?)\//g, '../')
|
|
31
|
+
.replace(/leaflet-rotate@(.*?)\//g, '../');
|
|
32
|
+
await route.fulfill({ response, body, headers: response.headers() });
|
|
33
|
+
});
|
|
34
|
+
this.page = await this.context.newPage();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
After(async function () {
|
|
38
|
+
await this.context.close();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
Given('the example page is open', async function () {
|
|
42
|
+
await this.page.goto(`${baseUrl}/examples/leaflet-rotate.html`);
|
|
43
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
44
|
+
});
|
package/build/rollup.config.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import terser from "@rollup/plugin-terser";
|
|
2
|
-
import resolve from '@rollup/plugin-node-resolve';
|
|
3
|
-
import commonJS from '@rollup/plugin-commonjs';
|
|
4
|
-
|
|
5
|
-
import plugin from '../package.json' assert { type: "json" };
|
|
6
|
-
|
|
7
|
-
let input = "src/index.js";
|
|
8
|
-
let output = {
|
|
9
|
-
file: "dist/" + plugin.name + "-src.js",
|
|
10
|
-
format: "umd",
|
|
11
|
-
sourcemap: true,
|
|
12
|
-
name: plugin.name,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
let plugins = [
|
|
16
|
-
resolve(),
|
|
17
|
-
commonJS({
|
|
18
|
-
include: '../node_modules/**'
|
|
19
|
-
})
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
export default [{
|
|
23
|
-
input: input,
|
|
24
|
-
output: output,
|
|
25
|
-
plugins: plugins,
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
input: input,
|
|
29
|
-
output: Object.assign({}, output, {
|
|
30
|
-
file: "dist/" + plugin.name + ".js"
|
|
31
|
-
}),
|
|
32
|
-
plugins: plugins.concat(terser()),
|
|
33
|
-
}
|
|
34
|
-
];
|