@tomickigrzegorz/leaflet-rotate 0.1.2 → 0.1.4

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