leaflet-rotate 0.4.0 → 0.5.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.
@@ -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]];
@@ -33,22 +33,10 @@ Before(async function () {
33
33
  .replace(/\bconst (map|esri|osm|layers|markers|path|circle|polygon|rectangle) =/g, 'const $1 = window.$1 =');
34
34
  if (version === '2') {
35
35
  body = body
36
- .replace('<script src="https://unpkg.com/leaflet@1.9/dist/leaflet-src.js"></script>', `<script>
37
- globalThis.__leafletLoading = import('https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.js').then(async leaflet => {
38
- globalThis.L = Object.assign({}, leaflet);
39
- for (const src of ['../dist/leaflet-rotate-src.js', '../misc/route.js', '../misc/places.js', '../misc/loremIpsum.js', '../lib/debug.js']) {
40
- await new Promise((resolve, reject) => {
41
- const script = document.createElement('script');
42
- script.src = src;
43
- script.onload = resolve;
44
- script.onerror = () => reject(new Error(\`Could not load \${src}\`));
45
- document.head.appendChild(script);
46
- });
47
- }
48
- });
49
- </script>`)
50
- .replace(/<script src="\.\.\/(?:dist|misc|lib)\/[^"]+"><\/script>/g, '')
51
- .replace(/<script>\s*(const esri[\s\S]*?L\.Rotate\.debug\(map\);\s*)<\/script>/, '<script>globalThis.__leafletLoading.then(() => {$1});</script>')
36
+ .replace(
37
+ '<script src="https://unpkg.com/leaflet@1.9/dist/leaflet-src.js"></script>',
38
+ '<script src="https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet-global-src.js"></script><script>globalThis.L = globalThis.leaflet;</script>'
39
+ )
52
40
  .replace('https://unpkg.com/leaflet@1.9/dist/leaflet.css', 'https://unpkg.com/leaflet@2.0.0-alpha.1/dist/leaflet.css');
53
41
  }
54
42
  await route.fulfill({ response, body, headers: response.headers() });