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