@tomickigrzegorz/leaflet-rotate 0.1.0 → 0.1.1

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