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