@tomickigrzegorz/leaflet-rotate 0.1.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.
@@ -0,0 +1,1467 @@
1
+ /*! @tomickigrzegorz/leaflet-rotate v0.1.0 | MIT */
2
+ import L from 'leaflet';
3
+
4
+ // =====================================================================
5
+ // 1. L.Point — rotation helpers
6
+ // =====================================================================
7
+ L.Point.prototype.rotate = function (theta) {
8
+ var cos = Math.cos(theta),
9
+ sin = Math.sin(theta);
10
+ return new L.Point(
11
+ this.x * cos - this.y * sin,
12
+ this.x * sin + this.y * cos,
13
+ );
14
+ };
15
+
16
+ L.Point.prototype.rotateFrom = function (theta, pivot) {
17
+ if (!pivot) return this.rotate(theta);
18
+ return this.subtract(pivot).rotate(theta).add(pivot);
19
+ };
20
+
21
+ // =====================================================================
22
+ // 2. L.DomUtil — extended setTransform / setPosition
23
+ // =====================================================================
24
+ L.DomUtil.setTransform = function (el, offset, scale, bearing, pivot) {
25
+ var pos = offset || new L.Point(0, 0);
26
+ var transform = "translate3d(" + pos.x + "px," + pos.y + "px,0)";
27
+ if (scale !== undefined && scale !== null) {
28
+ transform += " scale(" + scale + ")";
29
+ }
30
+ if (bearing) {
31
+ transform += " rotate(" + bearing + "rad)";
32
+ }
33
+ el.style[L.DomUtil.TRANSFORM] = transform;
34
+ if (pivot) {
35
+ el.style[L.DomUtil.TRANSFORM + "Origin"] =
36
+ pivot.x + "px " + pivot.y + "px";
37
+ }
38
+ };
39
+
40
+ L.DomUtil.setPosition = function (el, point, bearing, pivot) {
41
+ el._leaflet_pos = point;
42
+ if (L.Browser.any3d) {
43
+ L.DomUtil.setTransform(el, point, undefined, bearing, pivot);
44
+ } else {
45
+ el.style.left = point.x + "px";
46
+ el.style.top = point.y + "px";
47
+ }
48
+ };
49
+
50
+ const DEG_TO_RAD = Math.PI / 180;
51
+ const RAD_TO_DEG = 180 / Math.PI;
52
+
53
+ // =====================================================================
54
+ // 3. L.Map — core rotation
55
+ // =====================================================================
56
+ var _mapProto$1 = L.Map.prototype;
57
+
58
+ L.Map.mergeOptions({
59
+ rotate: false,
60
+ bearing: 0,
61
+ touchRotate: false,
62
+ shiftKeyRotate: false,
63
+ dragRotate: true,
64
+ rotateControl: false,
65
+ rotateClockwise: true,
66
+ });
67
+
68
+ var _mapInitialize = _mapProto$1.initialize;
69
+ _mapProto$1.initialize = function (id, options) {
70
+ if (options && options.rotate) {
71
+ this._rotate = true;
72
+ this._bearing = 0;
73
+ this._bearingRad = 0;
74
+ }
75
+ _mapInitialize.call(this, id, options);
76
+ if (this._rotate) {
77
+ this.setBearing(options.bearing || 0);
78
+ }
79
+ };
80
+
81
+ // --- Pane hierarchy ---
82
+ var _initPanes = _mapProto$1._initPanes;
83
+ _mapProto$1._initPanes = function () {
84
+ _initPanes.call(this);
85
+ if (!this._rotate) return;
86
+
87
+ var mapPane = this._mapPane;
88
+ this._rotatePane = L.DomUtil.create("div", "leaflet-rotate-pane", mapPane);
89
+ this._norotatePane = L.DomUtil.create(
90
+ "div",
91
+ "leaflet-norotate-pane",
92
+ mapPane,
93
+ );
94
+
95
+ this._rotatePane.appendChild(this._panes.tilePane);
96
+ this._rotatePane.appendChild(this._panes.overlayPane);
97
+
98
+ this._norotatePane.appendChild(this._panes.shadowPane);
99
+ this._norotatePane.appendChild(this._panes.markerPane);
100
+ this._norotatePane.appendChild(this._panes.tooltipPane);
101
+ this._norotatePane.appendChild(this._panes.popupPane);
102
+
103
+ L.DomUtil.addClass(this._rotatePane, "leaflet-proxy leaflet-zoom-animated");
104
+ };
105
+
106
+ // --- setBearing / getBearing ---
107
+ _mapProto$1.setBearing = function (theta) {
108
+ if (!this._rotate) return;
109
+ this._commitRotatePan();
110
+ var bearing = ((theta % 360) + 360) % 360;
111
+ this._bearing = bearing;
112
+ this._bearingRad = bearing * DEG_TO_RAD;
113
+ this._updateRotatePaneTransform();
114
+ this.fire("rotate");
115
+ };
116
+
117
+ _mapProto$1.getBearing = function () {
118
+ return this._bearing || 0;
119
+ };
120
+
121
+ _mapProto$1._updateRotatePaneTransform = function () {
122
+ if (!this._rotatePane) return;
123
+ if (!this._bearing) {
124
+ this._rotatePane.style[L.DomUtil.TRANSFORM] = "";
125
+ this._rotatePane.style[L.DomUtil.TRANSFORM + "Origin"] = "";
126
+ return;
127
+ }
128
+ var size = this.getSize();
129
+ var viewHalf = size.divideBy(2);
130
+ this._rotatePane.style[L.DomUtil.TRANSFORM + "Origin"] =
131
+ viewHalf.x + "px " + viewHalf.y + "px";
132
+ this._rotatePane.style[L.DomUtil.TRANSFORM] =
133
+ "rotate(" + this._bearingRad + "rad)";
134
+ };
135
+
136
+ // After a pan, reproject so the map pane position returns to (0,0).
137
+ // Keeps the rotation pivot (transform-origin) at the viewport center.
138
+ _mapProto$1._commitRotatePan = function () {
139
+ if (!this._rotate || this._committingRotatePan) return;
140
+ var pos = this._getMapPanePos();
141
+ if (!pos || (pos.x === 0 && pos.y === 0)) return;
142
+ this._committingRotatePan = true;
143
+ this._resetView(this.getCenter(), this.getZoom(), true);
144
+ this._committingRotatePan = false;
145
+ };
146
+
147
+ // --- Coordinate transforms ---
148
+ // CSS rotation on rotatePane rotates around viewHalf (center of viewport).
149
+ // A layer point lp in rotatePane appears on screen at:
150
+ // containerPoint = (lp - viewHalf).rotate(bearing) + viewHalf + mapPanePos
151
+ // Inverse:
152
+ // layerPoint = (cp - mapPanePos - viewHalf).rotate(-bearing) + viewHalf
153
+
154
+ var _containerPointToLayerPoint = _mapProto$1.containerPointToLayerPoint;
155
+ _mapProto$1.containerPointToLayerPoint = function (point) {
156
+ if (!this._rotate || !this._bearing) {
157
+ return _containerPointToLayerPoint.call(this, point);
158
+ }
159
+ var cp = L.point(point);
160
+ var mapPanePos = this._getMapPanePos();
161
+ var viewHalf = this.getSize().divideBy(2);
162
+ return cp
163
+ .subtract(mapPanePos)
164
+ .subtract(viewHalf)
165
+ .rotate(-this._bearingRad)
166
+ .add(viewHalf);
167
+ };
168
+
169
+ var _layerPointToContainerPoint = _mapProto$1.layerPointToContainerPoint;
170
+ _mapProto$1.layerPointToContainerPoint = function (point) {
171
+ if (!this._rotate || !this._bearing) {
172
+ return _layerPointToContainerPoint.call(this, point);
173
+ }
174
+ var lp = L.point(point);
175
+ var mapPanePos = this._getMapPanePos();
176
+ var viewHalf = this.getSize().divideBy(2);
177
+ return lp
178
+ .subtract(viewHalf)
179
+ .rotate(this._bearingRad)
180
+ .add(viewHalf)
181
+ .add(mapPanePos);
182
+ };
183
+
184
+ // --- rotatedPointToMapPanePoint ---
185
+ // Converts a layer point (rotatePane coords) to norotatePane coords.
186
+ // Marker is in norotatePane, so its position = lp.rotateFrom(bearing, viewHalf)
187
+ _mapProto$1.rotatedPointToMapPanePoint = function (point) {
188
+ if (!this._bearing) return L.point(point);
189
+ var viewHalf = this.getSize().divideBy(2);
190
+ return L.point(point).rotateFrom(this._bearingRad, viewHalf);
191
+ };
192
+
193
+ _mapProto$1.mapPanePointToRotatedPoint = function (point) {
194
+ if (!this._bearing) return L.point(point);
195
+ var viewHalf = this.getSize().divideBy(2);
196
+ return L.point(point).rotateFrom(-this._bearingRad, viewHalf);
197
+ };
198
+
199
+ // --- _getCenterOffset ---
200
+ // Returns screen-space offset for panBy to work correctly with rotation.
201
+ var _getCenterOffset = _mapProto$1._getCenterOffset;
202
+ _mapProto$1._getCenterOffset = function (latlng) {
203
+ if (!this._rotate || !this._bearing) {
204
+ return _getCenterOffset.call(this, latlng);
205
+ }
206
+ var dp = this.project(latlng).subtract(this.project(this.getCenter()));
207
+ return dp.rotate(this._bearingRad);
208
+ };
209
+
210
+ // --- getBounds with 4 corners ---
211
+ var _getBounds = _mapProto$1.getBounds;
212
+ _mapProto$1.getBounds = function () {
213
+ if (!this._rotate || !this._bearing) {
214
+ return _getBounds.call(this);
215
+ }
216
+ var size = this.getSize();
217
+ var bounds = L.latLngBounds();
218
+ bounds.extend(this.containerPointToLatLng(L.point(0, 0)));
219
+ bounds.extend(this.containerPointToLatLng(L.point(size.x, 0)));
220
+ bounds.extend(this.containerPointToLatLng(L.point(size.x, size.y)));
221
+ bounds.extend(this.containerPointToLatLng(L.point(0, size.y)));
222
+ return bounds;
223
+ };
224
+
225
+ _mapProto$1.mapBoundsToContainerBounds = function (bounds) {
226
+ return L.bounds([
227
+ this.latLngToContainerPoint(bounds.getNorthWest()),
228
+ this.latLngToContainerPoint(bounds.getNorthEast()),
229
+ this.latLngToContainerPoint(bounds.getSouthEast()),
230
+ this.latLngToContainerPoint(bounds.getSouthWest()),
231
+ ]);
232
+ };
233
+
234
+ var _getBoundsZoom = _mapProto$1.getBoundsZoom;
235
+ _mapProto$1.getBoundsZoom = function (bounds, inside, padding) {
236
+ if (!this._rotate || !this._bearing) {
237
+ return _getBoundsZoom.call(this, bounds, inside, padding);
238
+ }
239
+ bounds = L.latLngBounds(bounds);
240
+ padding = L.point(padding || [0, 0]);
241
+ var zoom = this.getZoom() || 0;
242
+ var min = this.getMinZoom();
243
+ var max = this.getMaxZoom();
244
+ var size = this.getSize().subtract(padding);
245
+ if (size.x <= 0 || size.y <= 0) return zoom;
246
+ var containerBounds = this.mapBoundsToContainerBounds(bounds);
247
+ var boundsSize = containerBounds.getSize();
248
+ var snap = this.options.zoomSnap;
249
+ var scaleX = size.x / boundsSize.x;
250
+ var scaleY = size.y / boundsSize.y;
251
+ var scale = inside ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
252
+ zoom = this.getScaleZoom(scale, zoom);
253
+ if (snap) zoom = Math.round(zoom / snap) * snap;
254
+ return Math.max(min, Math.min(max, zoom));
255
+ };
256
+
257
+ // --- _animateZoomNoDelay (PR#61 fix) ---
258
+ _mapProto$1._animateZoomNoDelay = function (center, zoom, startAnim) {
259
+ if (!this._mapPane) return;
260
+ if (startAnim) {
261
+ this._animatingZoom = true;
262
+ this._animateToCenter = center;
263
+ this._animateToZoom = zoom;
264
+ }
265
+ this._move(this._animateToCenter, this._animateToZoom, undefined, true);
266
+ this._onZoomTransitionEnd();
267
+ };
268
+
269
+ // --- Smooth (animated) zoom while rotated ---
270
+ // The animated zoom path is rotation-correct (rotation-aware _getCenterOffset,
271
+ // renderer _updateTransform, marker/popup _animateZoom) as long as the map
272
+ // pane offset is zero. A leftover pan offset gave a wrong center + gray tiles,
273
+ // so commit the pan (reproject to mapPanePos = 0, visually identical) before
274
+ // animating, then let the standard animation run.
275
+ var _tryAnimatedZoom = _mapProto$1._tryAnimatedZoom;
276
+ _mapProto$1._tryAnimatedZoom = function (center, zoom, options) {
277
+ if (this._rotate && this._bearing && !this._animatingZoom) {
278
+ var pos = this._getMapPanePos();
279
+ if (pos && (pos.x || pos.y)) {
280
+ this._resetView(this.getCenter(), this.getZoom(), true);
281
+ }
282
+ }
283
+ return _tryAnimatedZoom.call(this, center, zoom, options);
284
+ };
285
+
286
+ // --- Resize handler: update transform-origin ---
287
+ L.Map.addInitHook(function () {
288
+ if (this._rotate) {
289
+ this.on("resize", this._updateRotatePaneTransform, this);
290
+ }
291
+ });
292
+
293
+ // =====================================================================
294
+ // 4. L.GridLayer — tile loading with rotation
295
+ // =====================================================================
296
+ var _gridGetEvents = L.GridLayer.prototype.getEvents;
297
+ L.GridLayer.prototype.getEvents = function () {
298
+ var events = _gridGetEvents.call(this);
299
+ if (this._map && this._map._rotate) {
300
+ events.rotate = this._onRotate;
301
+ }
302
+ return events;
303
+ };
304
+
305
+ L.GridLayer.prototype._onRotate = function () {
306
+ this._update();
307
+ };
308
+
309
+ var _getTiledPixelBounds = L.GridLayer.prototype._getTiledPixelBounds;
310
+ L.GridLayer.prototype._getTiledPixelBounds = function (center) {
311
+ if (!this._map._rotate || !this._map._bearing) {
312
+ return _getTiledPixelBounds.call(this, center);
313
+ }
314
+ var map = this._map;
315
+ var mapZoom = map._animatingZoom
316
+ ? Math.max(map._animateToZoom, map.getZoom())
317
+ : map.getZoom();
318
+ var scale = map.getZoomScale(mapZoom, this._tileZoom);
319
+ var pixelCenter = map.project(center, this._tileZoom).floor();
320
+ // Clamp scale to <=1 so zoom-out still loads the full (larger) target
321
+ // view; otherwise fast wheel zoom-out left gray gaps.
322
+ var halfSize = map
323
+ .getSize()
324
+ .divideBy(Math.min(scale, 1) * 2)
325
+ .multiplyBy(1.25);
326
+
327
+ var bounds = new L.Bounds();
328
+ var corners = [
329
+ L.point(-halfSize.x, -halfSize.y),
330
+ L.point(halfSize.x, -halfSize.y),
331
+ L.point(halfSize.x, halfSize.y),
332
+ L.point(-halfSize.x, halfSize.y),
333
+ ];
334
+ for (var i = 0; i < 4; i++) {
335
+ bounds.extend(pixelCenter.add(corners[i].rotate(-map._bearingRad)));
336
+ }
337
+ return bounds;
338
+ };
339
+
340
+ // =====================================================================
341
+ // 5. L.Renderer (Canvas + SVG) — rotation support
342
+ // =====================================================================
343
+ var _rendererOnAdd = L.Renderer.prototype.onAdd;
344
+ L.Renderer.prototype.onAdd = function (map) {
345
+ _rendererOnAdd.call(this, map);
346
+ if (map._rotate) {
347
+ L.DomUtil.addClass(this._container, "leaflet-zoom-animated");
348
+ this.options.padding = Math.max(this.options.padding || 0, 1.5);
349
+ }
350
+ };
351
+
352
+ var _rendererUpdateTransform = L.Renderer.prototype._updateTransform;
353
+ L.Renderer.prototype._updateTransform = function (center, zoom) {
354
+ if (!this._map || !this._map._rotate) {
355
+ return _rendererUpdateTransform.call(this, center, zoom);
356
+ }
357
+ if (!this._bounds || !this._boundsMinLatLng) return;
358
+ var map = this._map;
359
+ var scale = map.getZoomScale(zoom, this._zoom);
360
+ var offset = map._latLngToNewLayerPoint(this._boundsMinLatLng, zoom, center);
361
+ L.DomUtil.setTransform(this._container, offset, scale);
362
+ };
363
+
364
+ var _rendererUpdate = L.Renderer.prototype._update;
365
+ L.Renderer.prototype._update = function () {
366
+ if (!this._map || !this._map._rotate) {
367
+ return _rendererUpdate.call(this);
368
+ }
369
+
370
+ // Rotation-invariant bounds: a square centered on the view whose radius
371
+ // covers the padded viewport at ANY bearing. Independent of bearing, so
372
+ // the SVG isn't re-sized every rotation frame (avoids flicker) and is
373
+ // large enough to never clip shapes when rotated.
374
+ var p = Math.max(this.options.padding || 0, 1.5);
375
+ var map = this._map;
376
+ var size = map.getSize();
377
+ var center = map.containerPointToLayerPoint(size.divideBy(2));
378
+ var half = size.multiplyBy(0.5 + p);
379
+ var r = Math.ceil(Math.sqrt(half.x * half.x + half.y * half.y));
380
+
381
+ this._bounds = new L.Bounds(
382
+ center.subtract([r, r]).round(),
383
+ center.add([r, r]).round(),
384
+ );
385
+ this._center = map.getCenter();
386
+ this._zoom = map.getZoom();
387
+ // Latlng of bounds.min captured while renderer zoom == map zoom, so
388
+ // _updateTransform can reproject it even after map._zoom changed (pinch).
389
+ this._boundsMinLatLng = map.layerPointToLatLng(this._bounds.min);
390
+ };
391
+
392
+ const _mapProto = L.Map.prototype;
393
+
394
+ // --- Heading-up: smooth, source-agnostic ---
395
+ // Any provider (geolocation, LocateControl, ...) feeds a heading in degrees
396
+ // (0 = N, clockwise). An internal rAF loop eases the bearing toward
397
+ // heading-up so a flood of updates never makes the map jump.
398
+ _mapProto.setHeading = function (deg, options) {
399
+ if (!this._rotate) return this;
400
+ if (deg === null || deg === undefined || isNaN(deg)) {
401
+ return this.stopHeadingUp();
402
+ }
403
+ options = options || {};
404
+ this._headingUp = true;
405
+ this._headingEase = options.ease != null ? options.ease : 0.2;
406
+ this._headingDeadzone =
407
+ options.deadzone != null ? options.deadzone : 0.5;
408
+ // Heading direction must point to the top of the screen: bearing = -heading.
409
+ this._headingTarget = (((-deg % 360) + 360) % 360);
410
+ this._startHeadingAnim();
411
+ return this;
412
+ };
413
+
414
+ _mapProto.stopHeadingUp = function () {
415
+ this._headingUp = false;
416
+ if (this._headingRAF) {
417
+ L.Util.cancelAnimFrame(this._headingRAF);
418
+ this._headingRAF = null;
419
+ }
420
+ return this;
421
+ };
422
+
423
+ _mapProto.getHeadingUp = function () {
424
+ return !!this._headingUp;
425
+ };
426
+
427
+ _mapProto._startHeadingAnim = function () {
428
+ if (this._headingRAF) return;
429
+ this._headingRAF = L.Util.requestAnimFrame(this._headingAnim, this);
430
+ };
431
+
432
+ _mapProto._headingAnim = function () {
433
+ this._headingRAF = null;
434
+ if (!this._headingUp) return;
435
+ var current = this.getBearing();
436
+ var diff = this._headingTarget - current;
437
+ while (diff > 180) diff -= 360;
438
+ while (diff < -180) diff += 360;
439
+ if (Math.abs(diff) < this._headingDeadzone) {
440
+ if (Math.abs(diff) > 0.001) this.setBearing(this._headingTarget);
441
+ return; // settled; loop restarts on next setHeading
442
+ }
443
+ this.setBearing(current + diff * this._headingEase);
444
+ this._headingRAF = L.Util.requestAnimFrame(this._headingAnim, this);
445
+ };
446
+
447
+ // =====================================================================
448
+ // 6. L.Marker — rotation-aware positioning
449
+ // =====================================================================
450
+ L.Marker.mergeOptions({
451
+ rotation: 0,
452
+ rotateWithView: false,
453
+ scale: undefined,
454
+ });
455
+
456
+ var _markerGetEvents = L.Marker.prototype.getEvents;
457
+ L.Marker.prototype.getEvents = function () {
458
+ var events = _markerGetEvents.call(this);
459
+ if (this._map && this._map._rotate) {
460
+ events.rotate = this._rotateReposition;
461
+ events.rotateend = this._rotateEnd;
462
+ }
463
+ return events;
464
+ };
465
+
466
+ var _markerSetPos = L.Marker.prototype._setPos;
467
+ L.Marker.prototype._setPos = function (pos) {
468
+ if (this._map && this._map._rotate && this._map._bearing) {
469
+ pos = this._map.rotatedPointToMapPanePoint(pos);
470
+ }
471
+ if (_markerSetPos) {
472
+ _markerSetPos.call(this, pos);
473
+ return;
474
+ }
475
+ L.DomUtil.setPosition(this._icon, pos);
476
+ if (this._shadow) {
477
+ L.DomUtil.setPosition(this._shadow, pos);
478
+ }
479
+ this._zIndex = pos.y + this.options.zIndexOffset;
480
+ this._resetZIndex();
481
+ };
482
+
483
+ var _markerUpdate = L.Marker.prototype.update;
484
+ L.Marker.prototype.update = function () {
485
+ var result = _markerUpdate.call(this);
486
+ if (this._icon && this._map) {
487
+ var rotation = this.options.rotation || 0;
488
+ if (this.options.rotateWithView) {
489
+ rotation += this._map._bearing;
490
+ }
491
+ if (rotation || this.options.scale) {
492
+ var pos = L.DomUtil.getPosition(this._icon) || new L.Point(0, 0);
493
+ var transform = "translate3d(" + pos.x + "px," + pos.y + "px,0)";
494
+ if (rotation) {
495
+ transform += " rotate(" + rotation + "deg)";
496
+ }
497
+ if (this.options.scale) {
498
+ transform += " scale(" + this.options.scale + ")";
499
+ }
500
+ this._icon.style[L.DomUtil.TRANSFORM] = transform;
501
+ }
502
+ }
503
+ return result;
504
+ };
505
+
506
+ // C: Fast per-frame reposition during rotation. center/zoom are constant
507
+ // while rotating, so the layer point is cached and only the bearing is
508
+ // re-applied — skips latLngToLayerPoint projection per marker per frame.
509
+ L.Marker.prototype._rotateReposition = function () {
510
+ var map = this._map;
511
+ if (!map || !this._icon) return;
512
+ var lp;
513
+ if (map._rotInertia && this._rotLayerPt) {
514
+ lp = this._rotLayerPt;
515
+ } else {
516
+ lp = map.latLngToLayerPoint(this._latlng);
517
+ if (map._rotInertia) this._rotLayerPt = lp;
518
+ }
519
+ this._setPos(lp);
520
+ var rotation = this.options.rotation || 0;
521
+ if (this.options.rotateWithView) {
522
+ rotation += map._bearing;
523
+ }
524
+ if (rotation || this.options.scale) {
525
+ var pos = L.DomUtil.getPosition(this._icon) || new L.Point(0, 0);
526
+ var transform = "translate3d(" + pos.x + "px," + pos.y + "px,0)";
527
+ if (rotation) {
528
+ transform += " rotate(" + rotation + "deg)";
529
+ }
530
+ if (this.options.scale) {
531
+ transform += " scale(" + this.options.scale + ")";
532
+ }
533
+ this._icon.style[L.DomUtil.TRANSFORM] = transform;
534
+ }
535
+ };
536
+
537
+ // Rotation session ended: drop the cache and do a full update (which now
538
+ // also flushes the deferred z-index, since _rotating is cleared first).
539
+ L.Marker.prototype._rotateEnd = function () {
540
+ this._rotLayerPt = null;
541
+ this.update();
542
+ };
543
+
544
+ // B: Defer z-index writes while a rotation session is active. Skipping the
545
+ // per-frame style.zIndex write avoids stacking-context recalcs ×N markers.
546
+ var _markerResetZIndex = L.Marker.prototype._resetZIndex;
547
+ L.Marker.prototype._resetZIndex = function () {
548
+ if (this._map && this._map._rotating) return;
549
+ return _markerResetZIndex.call(this);
550
+ };
551
+
552
+ // =====================================================================
553
+ // 7. L.Icon — transform-origin on anchor
554
+ // =====================================================================
555
+ var _setIconStyles = L.Icon.prototype._setIconStyles;
556
+ L.Icon.prototype._setIconStyles = function (img, name) {
557
+ _setIconStyles.call(this, img, name);
558
+ var anchor = this.options.iconAnchor || this.options.shadowAnchor;
559
+ if (anchor) {
560
+ img.style[L.DomUtil.TRANSFORM + "Origin"] =
561
+ anchor[0] + "px " + anchor[1] + "px";
562
+ }
563
+ };
564
+
565
+ // =====================================================================
566
+ // 8. L.DivOverlay / L.Popup / L.Tooltip — rotation support
567
+ // =====================================================================
568
+ if (L.DivOverlay) {
569
+ var _divOverlayGetEvents = L.DivOverlay.prototype.getEvents;
570
+ L.DivOverlay.prototype.getEvents = function () {
571
+ var events = _divOverlayGetEvents.call(this);
572
+ if (this._map && this._map._rotate) {
573
+ events.rotate = this._updatePosition;
574
+ }
575
+ return events;
576
+ };
577
+ }
578
+
579
+ if (L.Popup) {
580
+ var _popupUpdatePosition = L.Popup.prototype._updatePosition;
581
+ L.Popup.prototype._updatePosition = function () {
582
+ if (!this._map) return;
583
+ if (!this._map._rotate || !this._map._bearing) {
584
+ return _popupUpdatePosition.call(this);
585
+ }
586
+
587
+ var pos = this._map.latLngToLayerPoint(this._latlng);
588
+ var rotatedPos = this._map.rotatedPointToMapPanePoint(pos);
589
+ var offset = L.point(this.options.offset);
590
+ var anchor = this._getAnchor();
591
+ L.DomUtil.setPosition(this._container, rotatedPos.add(anchor));
592
+
593
+ this._containerBottom = -offset.y;
594
+ this._containerLeft =
595
+ -Math.round(this._containerWidth / 2) + offset.x;
596
+ this._container.style.bottom = this._containerBottom + "px";
597
+ this._container.style.left = this._containerLeft + "px";
598
+ };
599
+
600
+ var _popupAnimateZoom = L.Popup.prototype._animateZoom;
601
+ L.Popup.prototype._animateZoom = function (e) {
602
+ if (!this._map || !this._map._rotate || !this._map._bearing) {
603
+ if (_popupAnimateZoom) return _popupAnimateZoom.call(this, e);
604
+ return;
605
+ }
606
+ var pos = this._map._latLngToNewLayerPoint(
607
+ this._latlng,
608
+ e.zoom,
609
+ e.center,
610
+ );
611
+ pos = this._map.rotatedPointToMapPanePoint(pos);
612
+ var anchor = this._getAnchor();
613
+ L.DomUtil.setPosition(this._container, pos.add(anchor));
614
+ };
615
+
616
+ var _popupAdjustPan = L.Popup.prototype._adjustPan;
617
+ L.Popup.prototype._adjustPan = function () {
618
+ if (this._map && this._map._rotate) return;
619
+ if (_popupAdjustPan) _popupAdjustPan.call(this);
620
+ };
621
+ }
622
+
623
+ if (L.Tooltip) {
624
+ var _tooltipUpdatePosition = L.Tooltip.prototype._updatePosition;
625
+ L.Tooltip.prototype._updatePosition = function () {
626
+ if (!this._map) return;
627
+ if (!this._map._rotate || !this._map._bearing) {
628
+ return _tooltipUpdatePosition.call(this);
629
+ }
630
+
631
+ var pos = this._map.latLngToLayerPoint(this._latlng);
632
+ this._setPosition(this._map.rotatedPointToMapPanePoint(pos));
633
+ };
634
+
635
+ var _tooltipAnimateZoom = L.Tooltip.prototype._animateZoom;
636
+ L.Tooltip.prototype._animateZoom = function (e) {
637
+ if (!this._map || !this._map._rotate || !this._map._bearing) {
638
+ if (_tooltipAnimateZoom) return _tooltipAnimateZoom.call(this, e);
639
+ return;
640
+ }
641
+ var pos = this._map._latLngToNewLayerPoint(
642
+ this._latlng,
643
+ e.zoom,
644
+ e.center,
645
+ );
646
+ this._setPosition(this._map.rotatedPointToMapPanePoint(pos));
647
+ };
648
+ }
649
+
650
+ // =====================================================================
651
+ // 9. Touch Gestures Handler — pinch zoom + rotate (Google Maps style)
652
+ // - Rotation has a deadzone threshold (~10°) so pinch-zoom works
653
+ // without accidental rotation
654
+ // - Anchor point: the geographic point under the midpoint between
655
+ // fingers stays under that midpoint throughout the gesture
656
+ // =====================================================================
657
+ L.Map.TouchGestures = L.Handler.extend({
658
+ _ROTATION_THRESHOLD: 30 * DEG_TO_RAD,
659
+ _SCALE_THRESHOLD: 0.04,
660
+ _SCALE_THRESHOLD_ROT: 0.12,
661
+ _MOVE_THRESHOLD: 4,
662
+ _ZOOM_EPS: 0.01, // skip reproject if zoom unchanged this frame
663
+ _PAN_EPS: 2, // skip reproject if midpoint barely moved (px)
664
+ _ZOOM_SNAP_STEP: 0, // quantize live zoom → fewer reprojects (0 = off; >0 makes zoom step visibly)
665
+
666
+ // --- Rotation inertia (momentum spin after release) ---
667
+ _ROT_INERTIA: true, // master switch for the test
668
+ _ROT_DECAY: 0.0018, // higher = stops faster (per ms)
669
+ _ROT_MIN_VELOCITY: 0.004, // deg/ms below which inertia stops
670
+ _ROT_MAX_VELOCITY: 1.2, // clamp deg/ms to avoid wild spins
671
+ _ROT_VELOCITY_SMOOTH: 0.4, // EMA weight for new velocity samples
672
+ _ROT_STALE_MS: 80, // ignore velocity if last move older than this
673
+
674
+ addHooks: function () {
675
+ L.DomEvent.on(
676
+ this._map._container,
677
+ "touchstart",
678
+ this._onTouchStart,
679
+ this,
680
+ );
681
+ L.DomEvent.on(this._map._container, "touchmove", this._onTouchMove, this);
682
+ L.DomEvent.on(
683
+ this._map._container,
684
+ "touchend touchcancel",
685
+ this._onTouchEnd,
686
+ this,
687
+ );
688
+ },
689
+
690
+ removeHooks: function () {
691
+ L.DomEvent.off(
692
+ this._map._container,
693
+ "touchstart",
694
+ this._onTouchStart,
695
+ this,
696
+ );
697
+ L.DomEvent.off(
698
+ this._map._container,
699
+ "touchmove",
700
+ this._onTouchMove,
701
+ this,
702
+ );
703
+ L.DomEvent.off(
704
+ this._map._container,
705
+ "touchend touchcancel",
706
+ this._onTouchEnd,
707
+ this,
708
+ );
709
+ },
710
+
711
+ _onTouchStart: function (e) {
712
+ // Any new touch (even a single-finger pan) must abort rotation inertia
713
+ // first, or its setBearing loop races the drag: tiles jump and the
714
+ // marker layer-point cache goes stale (markers lag, then snap back).
715
+ this._stopRotateInertia();
716
+ if (!e.touches || e.touches.length !== 2) {
717
+ this._active = false;
718
+ return;
719
+ }
720
+ var map = this._map;
721
+ // Only set the flag when WE disable dragging here. A re-entrant touchstart
722
+ // (finger added/jittered mid-gesture) finds dragging already disabled — do
723
+ // NOT clear the flag, or touchend won't re-enable it and pan stays dead.
724
+ if (map.dragging && map.dragging.enabled()) {
725
+ this._draggingWasEnabled = true;
726
+ map.dragging.disable();
727
+ }
728
+ if (map._stop) map._stop();
729
+ this._rotVelocity = 0;
730
+ this._lastRotTime = 0;
731
+ this._lastRotBearing = 0;
732
+ // A two-finger gesture = manual control. Kill the heading-up easing loop
733
+ // NOW (touchstart), before any gesture math. Otherwise its per-frame
734
+ // setBearing races the pinch's _move loop → markers/tiles drift (only with
735
+ // geolocation on). Previously stopped only past the 30° rotate threshold,
736
+ // so a pure pinch-zoom left the loop running.
737
+ map.stopHeadingUp();
738
+ // Absorb any pan offset (mapPanePos -> 0) before the pinch so the anchor
739
+ // math and _move don't double-apply it (otherwise content drifts).
740
+ map._commitRotatePan();
741
+ var p1 = map.mouseEventToContainerPoint(e.touches[0]);
742
+ var p2 = map.mouseEventToContainerPoint(e.touches[1]);
743
+
744
+ this._startDist = p1.distanceTo(p2);
745
+ if (this._startDist < 1) {
746
+ this._active = false;
747
+ return;
748
+ }
749
+
750
+ this._touchZoomCenter = map.options.touchZoom === "center";
751
+ this._centerPoint = map.getSize().divideBy(2);
752
+ this._startCenter = map.getCenter();
753
+ this._startMidpoint = this._touchZoomCenter
754
+ ? this._centerPoint
755
+ : p1.add(p2).divideBy(2);
756
+ this._startAngle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
757
+ this._startBearing = map.getBearing();
758
+ this._startBearingRad = map._bearingRad || 0;
759
+ this._startZoom = map.getZoom();
760
+ this._anchorLatLng = this._touchZoomCenter
761
+ ? this._startCenter
762
+ : map.containerPointToLatLng(this._startMidpoint);
763
+
764
+ this._moved = false;
765
+ this._active = true;
766
+ this._rotationActive = false;
767
+ this._scaleActive = false;
768
+ this.zoom = false;
769
+ this._lastMoveZoom = this._startZoom;
770
+ this._lastMoveMidpoint = this._startMidpoint;
771
+
772
+ L.DomEvent.preventDefault(e);
773
+ },
774
+
775
+ _onTouchMove: function (e) {
776
+ if (!e.touches || e.touches.length !== 2 || !this._active) return;
777
+ var map = this._map;
778
+ var p1 = map.mouseEventToContainerPoint(e.touches[0]);
779
+ var p2 = map.mouseEventToContainerPoint(e.touches[1]);
780
+ var midpoint = this._touchZoomCenter
781
+ ? this._centerPoint
782
+ : p1.add(p2).divideBy(2);
783
+ var dist = p1.distanceTo(p2);
784
+ var angle = Math.atan2(p2.y - p1.y, p2.x - p1.x);
785
+
786
+ var midpointDelta = midpoint.distanceTo(this._startMidpoint);
787
+
788
+ var scale = dist / this._startDist;
789
+ var scaleDelta = Math.abs(scale - 1);
790
+
791
+ var angleDelta = angle - this._startAngle;
792
+ while (angleDelta > Math.PI) angleDelta -= 2 * Math.PI;
793
+ while (angleDelta < -Math.PI) angleDelta += 2 * Math.PI;
794
+
795
+ var rotationBeyond =
796
+ map.options.touchRotate &&
797
+ Math.abs(angleDelta) > this._ROTATION_THRESHOLD;
798
+ var scaleBeyond =
799
+ scaleDelta >
800
+ (this._rotationActive
801
+ ? this._SCALE_THRESHOLD_ROT
802
+ : this._SCALE_THRESHOLD);
803
+ var moveBeyond = midpointDelta > this._MOVE_THRESHOLD;
804
+
805
+ if (!this._moved) {
806
+ if (!rotationBeyond && !scaleBeyond && !moveBeyond) {
807
+ L.DomEvent.preventDefault(e);
808
+ return;
809
+ }
810
+ map._moveStart(true, false);
811
+ this._moved = true;
812
+ }
813
+
814
+ // --- Zoom (with deadzone) ---
815
+ var newZoom = this._startZoom;
816
+ if (!this._scaleActive && scaleBeyond) {
817
+ this._scaleActive = true;
818
+ }
819
+ if (this._scaleActive) {
820
+ newZoom = map.getScaleZoom(scale, this._startZoom);
821
+ if (
822
+ !map.options.bounceAtZoomLimits &&
823
+ ((newZoom < map.getMinZoom() && scale < 1) ||
824
+ (newZoom > map.getMaxZoom() && scale > 1))
825
+ ) {
826
+ newZoom = Math.max(
827
+ map.getMinZoom(),
828
+ Math.min(map.getMaxZoom(), newZoom),
829
+ );
830
+ }
831
+ // Quantize live zoom so reproject fires only on step crossings, not on
832
+ // every micro finger-distance jitter while rotating.
833
+ if (this._ZOOM_SNAP_STEP > 0) {
834
+ newZoom =
835
+ Math.round(newZoom / this._ZOOM_SNAP_STEP) * this._ZOOM_SNAP_STEP;
836
+ }
837
+ }
838
+
839
+ // --- Rotation (only after threshold exceeded) ---
840
+
841
+ var newBearingRad = this._startBearingRad;
842
+ if (map.options.touchRotate) {
843
+ if (!this._rotationActive) {
844
+ if (Math.abs(angleDelta) > this._ROTATION_THRESHOLD) {
845
+ this._rotationActive = true;
846
+ this._rotRefAngle = angle;
847
+ map._rotating = true;
848
+ map.stopHeadingUp();
849
+ map.fire("rotatestart");
850
+ }
851
+ }
852
+ if (this._rotationActive) {
853
+ var rotDelta = angle - this._rotRefAngle;
854
+ while (rotDelta > Math.PI) rotDelta -= 2 * Math.PI;
855
+ while (rotDelta < -Math.PI) rotDelta += 2 * Math.PI;
856
+ var dir = map.options.rotateClockwise === false ? -1 : 1;
857
+ var newBearing = this._startBearing + dir * rotDelta * RAD_TO_DEG;
858
+ newBearing = ((newBearing % 360) + 360) % 360;
859
+ map.setBearing(newBearing);
860
+ newBearingRad = map._bearingRad || 0;
861
+
862
+ // Track angular velocity (deg/ms) for release inertia
863
+ var now =
864
+ (typeof performance !== "undefined" && performance.now
865
+ ? performance.now()
866
+ : Date.now());
867
+ if (this._lastRotTime) {
868
+ var dtRot = now - this._lastRotTime;
869
+ if (dtRot > 0) {
870
+ var db = newBearing - this._lastRotBearing;
871
+ while (db > 180) db -= 360;
872
+ while (db < -180) db += 360;
873
+ var sample = db / dtRot;
874
+ var w = this._ROT_VELOCITY_SMOOTH;
875
+ this._rotVelocity =
876
+ (1 - w) * (this._rotVelocity || 0) + w * sample;
877
+ }
878
+ }
879
+ this._lastRotTime = now;
880
+ this._lastRotBearing = newBearing;
881
+ }
882
+ }
883
+
884
+ // Gate the costly reproject: only when zoom or midpoint actually changed
885
+ // this frame. During pure rotation neither does → rotation stays as cheap
886
+ // as the mouse (just setBearing), no per-frame _move of all tiles/layers.
887
+ var zoomChanged =
888
+ Math.abs(newZoom - this._lastMoveZoom) > this._ZOOM_EPS;
889
+ var panChanged =
890
+ midpoint.distanceTo(this._lastMoveMidpoint) > this._PAN_EPS;
891
+
892
+ if (this._scaleActive && (zoomChanged || panChanged)) {
893
+ // --- Anchor: keep geographic point under midpoint ---
894
+ var viewHalf = map.getSize().divideBy(2);
895
+ var screenOffset = midpoint.subtract(viewHalf);
896
+ var pivotPixel = map.project(this._anchorLatLng, newZoom);
897
+ var centerPixel = pivotPixel.subtract(
898
+ screenOffset.rotate(-newBearingRad),
899
+ );
900
+ var newCenter = map.unproject(centerPixel, newZoom);
901
+
902
+ this._center = newCenter;
903
+ this._zoom = newZoom;
904
+ this.zoom = true;
905
+ this._lastMoveZoom = newZoom;
906
+ this._lastMoveMidpoint = midpoint;
907
+
908
+ if (this._animRequest) {
909
+ L.Util.cancelAnimFrame(this._animRequest);
910
+ }
911
+ var moveFn = L.Util.bind(
912
+ map._move,
913
+ map,
914
+ newCenter,
915
+ newZoom,
916
+ { pinch: true, round: false },
917
+ undefined,
918
+ );
919
+ this._animRequest = L.Util.requestAnimFrame(moveFn, this, true);
920
+ } else if (this._scaleActive) ; else {
921
+ this._center = this._startCenter;
922
+ this._zoom = this._startZoom;
923
+ this.zoom = false;
924
+ if (this._animRequest) {
925
+ L.Util.cancelAnimFrame(this._animRequest);
926
+ this._animRequest = null;
927
+ }
928
+ }
929
+
930
+ L.DomEvent.preventDefault(e);
931
+ },
932
+
933
+ _onTouchEnd: function (e) {
934
+ var map = this._map;
935
+ if (this._draggingWasEnabled && map.dragging) {
936
+ map.dragging.enable();
937
+ this._draggingWasEnabled = false;
938
+ }
939
+ if (!this._active) return;
940
+ this._active = false;
941
+ if (!this._moved) return;
942
+ if (this._animRequest) {
943
+ L.Util.cancelAnimFrame(this._animRequest);
944
+ this._animRequest = null;
945
+ }
946
+ if (this.zoom) {
947
+ if (map.options.zoomAnimation) {
948
+ map._animateZoom(
949
+ this._center,
950
+ map._limitZoom(this._zoom),
951
+ true,
952
+ map.options.zoomSnap,
953
+ );
954
+ } else {
955
+ map._resetView(this._center, map._limitZoom(this._zoom));
956
+ }
957
+ }
958
+ if (this._rotationActive) {
959
+ if (!this._startRotateInertia()) {
960
+ map._rotating = false;
961
+ map.fire("rotateend");
962
+ }
963
+ }
964
+ this.zoom = false;
965
+ },
966
+
967
+ _stopRotateInertia: function () {
968
+ if (this._rotInertiaReq) {
969
+ L.Util.cancelAnimFrame(this._rotInertiaReq);
970
+ this._rotInertiaReq = null;
971
+ }
972
+ var map = this._map;
973
+ if (map && map._rotating) {
974
+ map._rotating = false;
975
+ map._rotInertia = false;
976
+ map.fire("rotateend");
977
+ }
978
+ },
979
+
980
+ _startRotateInertia: function () {
981
+ var map = this._map;
982
+ if (!this._ROT_INERTIA) return false;
983
+
984
+ var now =
985
+ (typeof performance !== "undefined" && performance.now
986
+ ? performance.now()
987
+ : Date.now());
988
+ // Stale: finger held still before lifting → no fling
989
+ if (
990
+ !this._lastRotTime ||
991
+ now - this._lastRotTime > this._ROT_STALE_MS ||
992
+ Math.abs(this._rotVelocity || 0) < this._ROT_MIN_VELOCITY
993
+ ) {
994
+ return false;
995
+ }
996
+
997
+ var v = this._rotVelocity;
998
+ var cap = this._ROT_MAX_VELOCITY;
999
+ if (v > cap) v = cap;
1000
+ if (v < -cap) v = -cap;
1001
+
1002
+ var decay = this._ROT_DECAY;
1003
+ var minV = this._ROT_MIN_VELOCITY;
1004
+ var last = now;
1005
+ var self = this;
1006
+
1007
+ map._rotInertia = true;
1008
+ map.fire("rotatestart");
1009
+ var step = function () {
1010
+ var t =
1011
+ (typeof performance !== "undefined" && performance.now
1012
+ ? performance.now()
1013
+ : Date.now());
1014
+ var dt = t - last;
1015
+ last = t;
1016
+ if (dt <= 0) dt = 16;
1017
+
1018
+ v *= Math.exp(-decay * dt);
1019
+ if (Math.abs(v) < minV) {
1020
+ self._rotInertiaReq = null;
1021
+ map._rotating = false;
1022
+ map._rotInertia = false;
1023
+ map.fire("rotateend");
1024
+ return;
1025
+ }
1026
+ var b = map.getBearing() + v * dt;
1027
+ b = ((b % 360) + 360) % 360;
1028
+ map.setBearing(b);
1029
+ self._rotInertiaReq = L.Util.requestAnimFrame(step, self);
1030
+ };
1031
+ this._rotInertiaReq = L.Util.requestAnimFrame(step, this);
1032
+ return true;
1033
+ },
1034
+ });
1035
+
1036
+ L.Map.addInitHook("addHandler", "touchGestures", L.Map.TouchGestures);
1037
+
1038
+ L.Map.addInitHook(function () {
1039
+ if (this.options.rotate && this.options.touchRotate) {
1040
+ if (this.touchGestures) this.touchGestures.enable();
1041
+ if (this.touchZoom) this.touchZoom.disable();
1042
+ }
1043
+ });
1044
+
1045
+ // =====================================================================
1046
+ // 10. Shift+Wheel Handler — bearing via scroll
1047
+ // =====================================================================
1048
+ L.Map.ShiftKeyRotate = L.Handler.extend({
1049
+ _ROTATE_STEP: 5,
1050
+ _EASE: 0.2,
1051
+
1052
+ addHooks: function () {
1053
+ L.DomEvent.on(this._map._container, "wheel", this._onWheel, this);
1054
+ },
1055
+ removeHooks: function () {
1056
+ L.DomEvent.off(this._map._container, "wheel", this._onWheel, this);
1057
+ this._stopAnim();
1058
+ },
1059
+ _onWheel: function (e) {
1060
+ if (!e.shiftKey) return;
1061
+ L.DomEvent.stop(e);
1062
+ var map = this._map;
1063
+ map.stopHeadingUp();
1064
+ if (!this._animating) map.fire("rotatestart");
1065
+ var delta = L.DomEvent.getWheelDelta(e);
1066
+ var dir = map.options.rotateClockwise === false ? -1 : 1;
1067
+ var next = map.getBearing() - dir * delta * this._ROTATE_STEP;
1068
+ this._targetBearing = ((next % 360) + 360) % 360;
1069
+ if (!this._animating) {
1070
+ this._startAnim();
1071
+ }
1072
+ },
1073
+
1074
+ _startAnim: function () {
1075
+ if (this._animating) return;
1076
+ this._animating = true;
1077
+ this._animRequest = L.Util.requestAnimFrame(this._animate, this, true);
1078
+ },
1079
+
1080
+ _stopAnim: function () {
1081
+ if (this._animRequest) {
1082
+ L.Util.cancelAnimFrame(this._animRequest);
1083
+ this._animRequest = null;
1084
+ }
1085
+ this._animating = false;
1086
+ },
1087
+
1088
+ _animate: function () {
1089
+ if (!this._animating) return;
1090
+ if (this._targetBearing === undefined || this._targetBearing === null) {
1091
+ this._stopAnim();
1092
+ return;
1093
+ }
1094
+
1095
+ var map = this._map;
1096
+ var current = map.getBearing();
1097
+ var diff = this._targetBearing - current;
1098
+ if (diff > 180) diff -= 360;
1099
+ if (diff < -180) diff += 360;
1100
+
1101
+ if (Math.abs(diff) < 0.1) {
1102
+ map.setBearing(this._targetBearing);
1103
+ this._stopAnim();
1104
+ return;
1105
+ }
1106
+
1107
+ map.setBearing(current + diff * this._EASE);
1108
+ this._animRequest = L.Util.requestAnimFrame(this._animate, this, true);
1109
+ },
1110
+ });
1111
+
1112
+ L.Map.addInitHook("addHandler", "shiftKeyRotate", L.Map.ShiftKeyRotate);
1113
+
1114
+ L.Map.addInitHook(function () {
1115
+ if (this.options.rotate && this.options.shiftKeyRotate) {
1116
+ if (this.shiftKeyRotate) this.shiftKeyRotate.enable();
1117
+ }
1118
+ });
1119
+
1120
+ // Prevent standard scroll zoom when shift is held (to avoid zoom+rotate conflict)
1121
+ if (L.Map.ScrollWheelZoom) {
1122
+ var _scrollOnWheel = L.Map.ScrollWheelZoom.prototype._onWheelScroll;
1123
+ L.Map.ScrollWheelZoom.prototype._onWheelScroll = function (e) {
1124
+ if (
1125
+ e.shiftKey &&
1126
+ this._map &&
1127
+ this._map._rotate &&
1128
+ this._map.options.shiftKeyRotate
1129
+ )
1130
+ return;
1131
+ return _scrollOnWheel.call(this, e);
1132
+ };
1133
+ }
1134
+
1135
+ // =====================================================================
1136
+ // 10b. DragRotate Handler — right mouse button drag (MapLibre style)
1137
+ // Rotates the map around its center.
1138
+ // =====================================================================
1139
+ L.Map.DragRotate = L.Handler.extend({
1140
+ _SENSITIVITY: 0.5, // degrees per pixel of horizontal movement
1141
+
1142
+ addHooks: function () {
1143
+ L.DomEvent.on(this._map._container, "mousedown", this._onDown, this);
1144
+ L.DomEvent.on(
1145
+ this._map._container,
1146
+ "contextmenu",
1147
+ L.DomEvent.preventDefault,
1148
+ );
1149
+ },
1150
+ removeHooks: function () {
1151
+ L.DomEvent.off(this._map._container, "mousedown", this._onDown, this);
1152
+ L.DomEvent.off(
1153
+ this._map._container,
1154
+ "contextmenu",
1155
+ L.DomEvent.preventDefault,
1156
+ );
1157
+ this._cleanup();
1158
+ },
1159
+ _onDown: function (e) {
1160
+ if (e.button !== 2) return;
1161
+ L.DomEvent.preventDefault(e);
1162
+ L.DomEvent.stopPropagation(e);
1163
+ var map = this._map;
1164
+ this._startX = e.clientX;
1165
+ this._startBearing = map.getBearing();
1166
+ this._moved = false;
1167
+ if (map.dragging && map.dragging.enabled()) {
1168
+ this._draggingWasEnabled = true;
1169
+ map.dragging.disable();
1170
+ } else {
1171
+ this._draggingWasEnabled = false;
1172
+ }
1173
+ L.DomEvent.on(document, "mousemove", this._onMove, this);
1174
+ L.DomEvent.on(document, "mouseup", this._onUp, this);
1175
+ },
1176
+ _onMove: function (e) {
1177
+ var dx = e.clientX - this._startX;
1178
+ if (!this._moved && Math.abs(dx) < 2) return;
1179
+ if (!this._moved) this._map.fire("rotatestart");
1180
+ this._moved = true;
1181
+ this._map.stopHeadingUp();
1182
+ var dir = this._map.options.rotateClockwise === false ? -1 : 1;
1183
+ this._map.setBearing(this._startBearing + dir * dx * this._SENSITIVITY);
1184
+ },
1185
+ _onUp: function (e) {
1186
+ this._cleanup();
1187
+ if (this._draggingWasEnabled && this._map.dragging) {
1188
+ this._map.dragging.enable();
1189
+ }
1190
+ if (this._moved) {
1191
+ L.DomEvent.preventDefault(e);
1192
+ this._map.fire("rotate");
1193
+ }
1194
+ },
1195
+ _cleanup: function () {
1196
+ L.DomEvent.off(document, "mousemove", this._onMove, this);
1197
+ L.DomEvent.off(document, "mouseup", this._onUp, this);
1198
+ },
1199
+ });
1200
+
1201
+ L.Map.addInitHook("addHandler", "dragRotate", L.Map.DragRotate);
1202
+
1203
+ L.Map.addInitHook(function () {
1204
+ if (this.options.rotate && this.options.dragRotate) {
1205
+ if (this.dragRotate) this.dragRotate.enable();
1206
+ }
1207
+ });
1208
+
1209
+ // =====================================================================
1210
+ // 12. MarkerDrag fix — convert coords in rotated map
1211
+ // =====================================================================
1212
+ // Leaflet 1.9's MarkerDrag handler is an internal var, not exposed as
1213
+ // L.Handler.MarkerDrag, so patch its prototype lazily the first time a
1214
+ // marker builds its dragging handler (in _initInteraction).
1215
+ var _markerInitInteraction = L.Marker.prototype._initInteraction;
1216
+ L.Marker.prototype._initInteraction = function () {
1217
+ var result = _markerInitInteraction.call(this);
1218
+ if (this.dragging) {
1219
+ var proto = Object.getPrototypeOf(this.dragging);
1220
+ if (proto && proto._onDrag && !proto._rotateOnDragPatched) {
1221
+ proto._rotateOnDragPatched = true;
1222
+ var _markerDragOnDrag = proto._onDrag;
1223
+ proto._onDrag = function (e) {
1224
+ var marker = this._marker;
1225
+ var map = marker._map;
1226
+
1227
+ if (map && map._rotate && map._bearing) {
1228
+ var iconPos = L.DomUtil.getPosition(marker._icon);
1229
+ var layerPos = map.mapPanePointToRotatedPoint(iconPos);
1230
+ var latlng = map.layerPointToLatLng(layerPos);
1231
+
1232
+ if (marker._shadow) {
1233
+ L.DomUtil.setPosition(marker._shadow, iconPos);
1234
+ }
1235
+
1236
+ marker._latlng = latlng;
1237
+ e.latlng = latlng;
1238
+ e.oldLatLng = this._oldLatLng;
1239
+ marker.fire("move", e).fire("drag", e);
1240
+ return;
1241
+ }
1242
+
1243
+ return _markerDragOnDrag.call(this, e);
1244
+ };
1245
+ }
1246
+ }
1247
+ return result;
1248
+ };
1249
+
1250
+ // =====================================================================
1251
+ // 11. L.Control.Rotate — compass control
1252
+ // =====================================================================
1253
+ L.Control.Rotate = L.Control.extend({
1254
+ options: {
1255
+ position: "topleft",
1256
+ closeOnZeroBearing: true,
1257
+ },
1258
+
1259
+ onAdd: function (map) {
1260
+ var container = L.DomUtil.create(
1261
+ "div",
1262
+ "leaflet-control-rotate leaflet-bar",
1263
+ );
1264
+ var link = L.DomUtil.create(
1265
+ "a",
1266
+ "leaflet-control-rotate-toggle",
1267
+ container,
1268
+ );
1269
+ link.href = "#";
1270
+ link.title = "Reset rotation";
1271
+ link.setAttribute("role", "button");
1272
+ link.setAttribute("aria-label", "Reset rotation");
1273
+ link.innerHTML =
1274
+ '<svg viewBox="0 0 24 24" width="18" height="18" style="display:block;margin:auto;padding:3px">' +
1275
+ '<path d="M12 2L8 8h3v8h2V8h3L12 2z" fill="currentColor"/></svg>';
1276
+
1277
+ this._link = link;
1278
+ this._container = container;
1279
+
1280
+ L.DomEvent.disableClickPropagation(container);
1281
+ L.DomEvent.on(link, "click", this._resetBearing, this);
1282
+ map.on("rotate", this._updateDisplay, this);
1283
+ this._updateDisplay();
1284
+ return container;
1285
+ },
1286
+
1287
+ onRemove: function (map) {
1288
+ map.off("rotate", this._updateDisplay, this);
1289
+ L.DomEvent.off(this._link, "click", this._resetBearing, this);
1290
+ },
1291
+
1292
+ _resetBearing: function (e) {
1293
+ L.DomEvent.stop(e);
1294
+ this._map.setBearing(0);
1295
+ },
1296
+
1297
+ _updateDisplay: function () {
1298
+ if (!this._map || !this._link) return;
1299
+ var bearing = this._map.getBearing();
1300
+ this._link.style[L.DomUtil.TRANSFORM] = "rotate(" + -bearing + "deg)";
1301
+ if (this.options.closeOnZeroBearing) {
1302
+ this._container.style.display = bearing === 0 ? "none" : "";
1303
+ }
1304
+ },
1305
+ });
1306
+
1307
+ L.control.rotate = function (options) {
1308
+ return new L.Control.Rotate(options);
1309
+ };
1310
+
1311
+ L.Map.addInitHook(function () {
1312
+ if (this.options.rotate && this.options.rotateControl) {
1313
+ var opts =
1314
+ this.options.rotateControl === true ? {} : this.options.rotateControl;
1315
+ this.rotateControl = L.control.rotate(opts);
1316
+ this.addControl(this.rotateControl);
1317
+ }
1318
+ });
1319
+
1320
+ // =====================================================================
1321
+ // 11b. L.Control.RotateCompass — bottom-right compass toggle
1322
+ // =====================================================================
1323
+ L.Control.RotateCompass = L.Control.extend({
1324
+ options: {
1325
+ position: "bottomright",
1326
+ enabled: false,
1327
+ },
1328
+
1329
+ onAdd: function (map) {
1330
+ this._map = map;
1331
+
1332
+ var container = L.DomUtil.create(
1333
+ "div",
1334
+ "leaflet-control-rotate-compass leaflet-bar",
1335
+ );
1336
+ var link = L.DomUtil.create(
1337
+ "a",
1338
+ "leaflet-control-rotate-compass-toggle",
1339
+ container,
1340
+ );
1341
+ link.href = "#";
1342
+ link.title = "Map rotation";
1343
+ link.setAttribute("role", "button");
1344
+ link.setAttribute("aria-label", "Map rotation");
1345
+ link.innerHTML =
1346
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="22" height="22" fill-rule="evenodd" clip-rule="evenodd" style="display:block;transform-origin:center;transform-box:fill-box">' +
1347
+ '<path fill="#ebebeb" stroke="#333" stroke-width=".6" d="m11.81.44 3.6 11.27h-7.2z"/>' +
1348
+ '<path fill="#b95358" stroke="#333" stroke-width=".6" d="m11.81 23.18-3.6-11.27h7.2z"/>' +
1349
+ "</svg>";
1350
+ this._needle = link.firstChild;
1351
+
1352
+ this._link = link;
1353
+ this._container = container;
1354
+
1355
+ L.DomEvent.disableClickPropagation(container);
1356
+ L.DomEvent.on(link, "click", this._toggleRotation, this);
1357
+ map.on("rotate", this._updateDisplay, this);
1358
+ if (this.options.enabled) {
1359
+ this._enableRotation();
1360
+ } else {
1361
+ this._disableRotation();
1362
+ }
1363
+ return container;
1364
+ },
1365
+
1366
+ onRemove: function (map) {
1367
+ map.off("rotate", this._updateDisplay, this);
1368
+ L.DomEvent.off(this._link, "click", this._toggleRotation, this);
1369
+ },
1370
+
1371
+ _toggleRotation: function (e) {
1372
+ L.DomEvent.stop(e);
1373
+ if (this._enabled) {
1374
+ this._disableRotation();
1375
+ } else {
1376
+ this._enableRotation();
1377
+ }
1378
+ },
1379
+
1380
+ _disableRotation: function () {
1381
+ this._enabled = false;
1382
+ if (this._map.dragRotate) this._map.dragRotate.disable();
1383
+ if (this._map.touchGestures) this._map.touchGestures.disable();
1384
+ if (this._map.touchZoom) this._map.touchZoom.enable();
1385
+ if (this._map.shiftKeyRotate) this._map.shiftKeyRotate.disable();
1386
+ this._map.setBearing(0);
1387
+ this._updateDisplay();
1388
+ },
1389
+
1390
+ _enableRotation: function () {
1391
+ this._enabled = true;
1392
+ if (this._map.dragRotate && this._map.options.dragRotate) {
1393
+ this._map.dragRotate.enable();
1394
+ }
1395
+ if (this._map.touchGestures && this._map.options.touchRotate) {
1396
+ this._map.touchGestures.enable();
1397
+ if (this._map.touchZoom) this._map.touchZoom.disable();
1398
+ }
1399
+ if (this._map.shiftKeyRotate && this._map.options.shiftKeyRotate) {
1400
+ this._map.shiftKeyRotate.enable();
1401
+ }
1402
+ this._updateDisplay();
1403
+ },
1404
+
1405
+ _updateDisplay: function () {
1406
+ if (!this._map || !this._link) return;
1407
+ var bearing = this._map.getBearing();
1408
+ if (this._needle) {
1409
+ this._needle.style[L.DomUtil.TRANSFORM] = "rotate(" + -bearing + "deg)";
1410
+ }
1411
+ if (this._enabled) {
1412
+ L.DomUtil.removeClass(
1413
+ this._container,
1414
+ "leaflet-rotate-compass--inactive",
1415
+ );
1416
+ } else {
1417
+ L.DomUtil.addClass(this._container, "leaflet-rotate-compass--inactive");
1418
+ }
1419
+ },
1420
+ });
1421
+
1422
+ L.control.rotateCompass = function (options) {
1423
+ return new L.Control.RotateCompass(options);
1424
+ };
1425
+
1426
+ L.Map.addInitHook(function () {
1427
+ if (this.options.rotate && this.options.rotateCompassControl) {
1428
+ var opts =
1429
+ this.options.rotateCompassControl === true
1430
+ ? {}
1431
+ : this.options.rotateCompassControl;
1432
+ this.rotateCompassControl = L.control.rotateCompass(opts);
1433
+ this.addControl(this.rotateCompassControl);
1434
+ }
1435
+ });
1436
+
1437
+ // =====================================================================
1438
+ // 13. Block page pinch-zoom gestures (iOS Safari)
1439
+ // CSS touch-action is not enough on iOS — preventDefault on
1440
+ // gesturestart/change/end blocks zooming of the whole page.
1441
+ // =====================================================================
1442
+ L.Map.mergeOptions({ preventPageGestures: true });
1443
+
1444
+ L.Map.addInitHook(function () {
1445
+ if (!this.options.preventPageGestures) return;
1446
+ var events = ["gesturestart", "gesturechange", "gestureend"];
1447
+ var prevent = function (e) {
1448
+ e.preventDefault();
1449
+ };
1450
+ events.forEach(function (ev) {
1451
+ document.addEventListener(ev, prevent, { passive: false });
1452
+ });
1453
+ this.on("unload", function () {
1454
+ events.forEach(function (ev) {
1455
+ document.removeEventListener(ev, prevent, { passive: false });
1456
+ });
1457
+ });
1458
+ });
1459
+
1460
+ // Injects ONLY the structural pane CSS (required for rotation to work).
1461
+ // Control styling lives in dist/leaflet-rotate.css (optional import).
1462
+ const style = document.createElement("style");
1463
+ style.textContent = [
1464
+ ".leaflet-rotate-pane { position: absolute; top: 0; left: 0; will-change: transform; }",
1465
+ ".leaflet-norotate-pane { position: absolute; top: 0; left: 0; z-index: 600; }",
1466
+ ].join("\n");
1467
+ document.head.appendChild(style);