leaflet-rotate 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +85 -5
  2. package/dist/leaflet-rotate-src.js +1325 -254
  3. package/dist/leaflet-rotate-src.js.map +4 -4
  4. package/dist/leaflet-rotate.js +1 -1
  5. package/dist/leaflet-rotate.js.map +4 -4
  6. package/lib/debug.js +62 -88
  7. package/lib/debug.min.js +16 -1
  8. package/misc/leaflet-heat-rotate.js +7 -0
  9. package/package.json +1 -1
  10. package/src/control/Rotate.js +160 -103
  11. package/src/dom/DomUtil.js +23 -12
  12. package/src/dom/Draggable.js +5 -0
  13. package/src/geo/LatLngBounds.js +5 -0
  14. package/src/geometry/Bounds.js +5 -0
  15. package/src/geometry/Point.js +17 -3
  16. package/src/index.js +4 -1
  17. package/src/layer/DivOverlay.js +34 -5
  18. package/src/layer/Popup.js +14 -3
  19. package/src/layer/Tooltip.js +16 -0
  20. package/src/layer/marker/Icon.js +12 -0
  21. package/src/layer/marker/Marker.js +86 -7
  22. package/src/layer/tile/GridLayer.js +38 -3
  23. package/src/layer/vector/Renderer.js +88 -34
  24. package/src/map/Map.js +578 -116
  25. package/src/map/handler/CompassBearing.js +34 -1
  26. package/src/map/handler/ContainerMutation.js +18 -1
  27. package/src/map/handler/ShiftKeyRotate.js +204 -13
  28. package/src/map/handler/TouchGestures.js +108 -10
  29. package/src/map/handler/TouchPitch.js +53 -0
  30. package/src/map/handler/TouchRotate.js +18 -1
  31. package/src/map/handler/TouchZoom.js +21 -1
  32. package/test/features/example.feature +8 -4
  33. package/test/features/gestures.feature +4 -0
  34. package/test/features/rotation.feature +60 -0
  35. package/test/index.html +342 -186
  36. package/test/steps/bearing.steps.js +535 -0
  37. package/test/steps/example.steps.js +56 -29
  38. package/test/steps/gestures.steps.js +55 -0
  39. package/test/support/hooks.js +4 -16
package/lib/debug.js CHANGED
@@ -8,21 +8,31 @@
8
8
  this._map = map;
9
9
 
10
10
  document.head.insertAdjacentHTML('beforeend', `<style>
11
- .leaflet-rotate-info {
12
- width: 100%;
11
+ .leaflet-rotate-tools {
12
+ display: flex;
13
+ flex-wrap: wrap;
14
+ align-items: center;
15
+ gap: 4px 16px;
16
+ margin-bottom: 6px;
13
17
  }
14
- .leaflet-rotate-info td {
15
- /* width: 7em; */
16
- overflow: hidden;
17
- /* max-width: 7em; */
18
- text-overflow: ellipsis;
19
- /* white-space: nowrap; */
18
+ .leaflet-rotate-tools input, .leaflet-rotate-tools button {
19
+ vertical-align: middle;
20
20
  }
21
- .leaflet-rotate-info td:nth-of-type(3) {
22
- font-family: Monospace;
21
+ .leaflet-rotate-info {
22
+ border-collapse: collapse;
23
23
  }
24
- .leaflet-rotate-info th {
24
+ .leaflet-rotate-info td, .leaflet-rotate-info th {
25
+ padding: 1px 12px 1px 0;
25
26
  text-align: left;
27
+ white-space: nowrap;
28
+ vertical-align: top;
29
+ }
30
+ .leaflet-rotate-info td:nth-of-type(1), .leaflet-rotate-info td:nth-of-type(2) {
31
+ font-family: monospace;
32
+ min-width: 12ch;
33
+ }
34
+ .leaflet-rotate-info tr[title] {
35
+ cursor: help;
26
36
  }
27
37
  div.crosshair {
28
38
  position:absolute;
@@ -115,73 +125,35 @@
115
125
  `<div class="crosshair c1"></div><div class="crosshair c2"></div>`
116
126
  );
117
127
 
118
- map._container.insertAdjacentHTML('afterend', `<hr>
128
+ const html = `
129
+ <div class="leaflet-rotate-tools">
130
+ <span>Bearing
131
+ ${[0, 15, 30, 45, 60, 75, 90, 180].map(b => `<button onclick="L.Rotate._map.setBearing(${b});" title="map.setBearing(${b});">${b}</button>`).join('')}
132
+ <input title="increase/decrease rotation angle" type="range" min="0" max="360" step="1" value="0" name="rho" id='rho_input' /><span id='rho'></span>
133
+ </span>
134
+ <label>Bounds padding <input title="increase/decrease padding bounds" type="range" min="-0.25" max="0" value="0" step="0.01" name="pad" id='pad_input' /></label>
135
+ <span>Markers
136
+ <button onclick="L.Rotate.randomRotateMarkers(L.Rotate._map);" title="markers.setRotation(Math.random() * Math.PI / 30 + 0.1)">Rotate</button>
137
+ <button onclick="L.Rotate.randomMarkers(L.Rotate._map, 100);" title="add 100 random markers to map">Add 100</button>
138
+ </span>
139
+ </div>
119
140
  <table class="leaflet-rotate-info">
120
- <tr>
121
- <td>
122
- <button onclick="L.Rotate._map.setBearing(0);" title="map.setBearing(0);"> 0</button>
123
- <button onclick="L.Rotate._map.setBearing(15);" title="map.setBearing(15);">15</button>
124
- <button onclick="L.Rotate._map.setBearing(30);" title="map.setBearing(30);">30</button>
125
- <button onclick="L.Rotate._map.setBearing(45);" title="map.setBearing(45);">45</button>
126
- <button onclick="L.Rotate._map.setBearing(60);" title="map.setBearing(60);">60</button>
127
- <button onclick="L.Rotate._map.setBearing(75);" title="map.setBearing(75);">75</button>
128
- <button onclick="L.Rotate._map.setBearing(90);" title="map.setBearing(90);">90</button>
129
- <button onclick="L.Rotate._map.setBearing(180);" title="map.setBearing(180);">180</button>
130
- </td>
131
- <td>
132
- <input title="increase/decrease rotation angle" type="range" min="0" max="360" step="1" value="0" name="rho" id='rho_input' /><span id='rho'></span>
133
- </td>
134
- <td>
135
- <input title="increase/decrease padding bounds" type="range" min="-0.25" max="0" value="0" step="0.01" name="pad" id='pad_input' />
136
- </td>
137
- <td>
138
- <button onclick="L.Rotate.randomRotateMarkers(L.Rotate._map);" title="markers.setRotation(Math.random() * Math.PI / 30 + 0.1)">Rotate Markers</button>
139
- <button onclick="L.Rotate.randomMarkers(L.Rotate._map, 100);" title="add 100 random markers to map">Add Markers</button>
140
- </td>
141
- </tr>
142
- <tr>
143
- <th>LatLng </th>
144
- <td id='llx'></td>
145
- <td id='lly'></td>
146
- <td class='long'>LatLng of mouse pointer</td>
147
- </tr>
148
- <tr>
149
- <th>Rel to pane </th>
150
- <td id='lyx'></td>
151
- <td id='lyy'></td>
152
- <td class='long'>Mouse pointer pixel coords relative to _rotatePane</td>
153
- </tr>
154
- <tr>
155
- <th>Rel to container</th>
156
- <td id='cnx'></td>
157
- <td id='cny'></td>
158
- <td class='long'>Mouse pointer coords relative to map &lt;div&gt;</td>
159
- </tr>
160
- <tr>
161
- <th>Pivot </th>
162
- <td id='pvx'></td>
163
- <td id='pvy'></td>
164
- <td class='long'>Last _rotationPane pivot pixel coords relative to _rotationPane</td>
165
- </tr>
166
- <tr>
167
- <th>Pane offset </th>
168
- <td id='pox'></td>
169
- <td id='poy'></td>
170
- <td class='long'>Pixel offset of _mapPane</td>
171
- </tr>
172
- <tr>
173
- <th>Pixel origin </th>
174
- <td id='ogx'></td>
175
- <td id='ogy'></td>
176
- <td class='long'>Negative pixel coords of the (0,0) CRS point relative to _rotatePane</td>
177
- </tr>
178
- <tr>
179
- <th>Pixel bounds </th>
180
- <td id='ogbx'></td>
181
- <td id='ogby'></td>
182
- <td class='long'>Bounds of the current map view in projected pixel coordinates {min, max}.</td>
183
- </tr>
184
- </table>`);
141
+ <tr><th></th><th>x / lng</th><th>y / lat</th></tr>
142
+ ${[
143
+ ['LatLng', 'LatLng of mouse pointer', 'llx', 'lly'],
144
+ ['Rel to pane', 'Mouse pointer pixel coords relative to _rotatePane', 'lyx', 'lyy'],
145
+ ['Rel to container', 'Mouse pointer coords relative to map <div>', 'cnx', 'cny'],
146
+ ['Pivot', 'Last _rotationPane pivot pixel coords relative to _rotationPane', 'pvx', 'pvy'],
147
+ ['Pane offset', 'Pixel offset of _mapPane', 'pox', 'poy'],
148
+ ['Pixel origin', 'Negative pixel coords of the (0,0) CRS point relative to _rotatePane', 'ogx', 'ogy'],
149
+ ['Pixel bounds (min / max)', 'Bounds of the current map view in projected pixel coordinates {min, max}', 'ogbx', 'ogby'],
150
+ ].map(([label, title, x, y]) => `<tr title="${title.replace('<', '&lt;').replace('>', '&gt;')}"><th>${label}</th><td id='${x}'></td><td id='${y}'></td></tr>`).join('')}
151
+ </table>`;
152
+ if (options.container) {
153
+ options.container.insertAdjacentHTML('beforeend', `<details open><summary>Debug</summary>${html}</details>`);
154
+ } else {
155
+ map._container.insertAdjacentHTML('afterend', '<hr>' + html);
156
+ }
185
157
 
186
158
  map.___rotatePixelOrigin = L.DomUtil.create('div', 'pixelorigin', map._rotatePane);
187
159
  map.___rotatePivotDot = L.DomUtil.create('div', 'pivot', map._mapPane);
@@ -260,12 +232,13 @@
260
232
  },
261
233
 
262
234
  logMouse(e) {
263
- document.getElementById('llx').innerHTML = e.latlng.lng;
264
- document.getElementById('lly').innerHTML = e.latlng.lat;
265
- document.getElementById('lyx').innerHTML = e.layerPoint.x;
266
- document.getElementById('lyy').innerHTML = e.layerPoint.y;
267
- document.getElementById('cnx').innerHTML = e.containerPoint.x;
268
- document.getElementById('cny').innerHTML = e.containerPoint.y;
235
+ const fmt = (n, d) => +n.toFixed(d);
236
+ document.getElementById('llx').innerHTML = fmt(e.latlng.lng, 6);
237
+ document.getElementById('lly').innerHTML = fmt(e.latlng.lat, 6);
238
+ document.getElementById('lyx').innerHTML = fmt(e.layerPoint.x, 1);
239
+ document.getElementById('lyy').innerHTML = fmt(e.layerPoint.y, 1);
240
+ document.getElementById('cnx').innerHTML = fmt(e.containerPoint.x, 1);
241
+ document.getElementById('cny').innerHTML = fmt(e.containerPoint.y, 1);
269
242
  },
270
243
 
271
244
  /**
@@ -288,8 +261,8 @@
288
261
  */
289
262
  updatePixelOrigin() {
290
263
  if (this._pixelOrigin) {
291
- document.getElementById('ogx').innerHTML = this._pixelOrigin.x;
292
- document.getElementById('ogy').innerHTML = this._pixelOrigin.y;
264
+ document.getElementById('ogx').innerHTML = +this._pixelOrigin.x.toFixed(1);
265
+ document.getElementById('ogy').innerHTML = +this._pixelOrigin.y.toFixed(1);
293
266
  this.___rotatePixelOrigin.style.left = (-this._pixelOrigin.x - 1) + 'px';
294
267
  this.___rotatePixelOrigin.style.top = (-this._pixelOrigin.y - 1) + 'px';
295
268
  }
@@ -530,8 +503,9 @@
530
503
  // ]);
531
504
  // // paddedSize = paddedBounds.getSize();
532
505
 
533
- document.getElementById('ogbx').innerHTML = JSON.stringify(pixelBounds.min);
534
- document.getElementById('ogby').innerHTML = JSON.stringify(pixelBounds.max);
506
+ const xy = p => `${+p.x.toFixed(1)}, ${+p.y.toFixed(1)}`;
507
+ document.getElementById('ogbx').innerHTML = xy(pixelBounds.min);
508
+ document.getElementById('ogby').innerHTML = xy(pixelBounds.max);
535
509
  },
536
510
 
537
511
  resetHeading(e) {
@@ -551,4 +525,4 @@
551
525
  }
552
526
 
553
527
  };
554
- })();
528
+ })();
package/lib/debug.min.js CHANGED
@@ -1 +1,16 @@
1
- (function(){const e=Object.isExtensible(globalThis.L)?globalThis.L:Object.assign({},globalThis.L);globalThis.L=e,e.Rotate={debug(t,n={}){this._map=t,document.head.insertAdjacentHTML("beforeend",'<style>.leaflet-rotate-info{width:100%}.leaflet-rotate-info td{overflow:hidden;text-overflow:ellipsis}.leaflet-rotate-info td:nth-of-type(3){font-family:Monospace}.leaflet-rotate-info th{text-align:left}div.crosshair{position:absolute;width:1px;height:1px;border:1px solid green;z-index:100;left:50%;right:50%}div.pivot,div.pixelorigin{position:absolute;width:1px;height:1px;border:5px solid yellow;border-radius:100%;z-index:1000}div.pivot:before{left:-1.5em;bottom:-2em;position:absolute;content:"PIVOT";color:#ff0}div.pixelorigin:before{left:-1.75em;top:-2em;position:absolute;content:"ORIGIN";color:#ff0}div.panebounds.main{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid red;border-bottom:1px red dashed;border-right:1px red dashed;z-index:100}div.panebounds.rotate{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid purple;border-bottom:1px purple dashed;border-right:1px purple dashed;z-index:100}div.crosshair.c1{left:calc(50% - 5px);top:50%;height:0;width:10px}div.crosshair.c2{left:50%;top:calc(50% - 5px);width:0;height:10px}#rho_input{vertical-align:middle}.panebounds:before{padding:1ch}.panebounds.rotate:before{float:right;content:"ROTATED"}.panebounds.main:before{float:left;content:"MAIN"}</style>'),t._container.insertAdjacentHTML("afterbegin",'<div class="crosshair c1"></div><div class="crosshair c2"></div>'),t._container.insertAdjacentHTML("afterend",`<hr><table class="leaflet-rotate-info"><tr><td><button onclick="L.Rotate._map.setBearing(0);" title="map.setBearing(0);"> 0</button><button onclick="L.Rotate._map.setBearing(15);" title="map.setBearing(15);">15</button><button onclick="L.Rotate._map.setBearing(30);" title="map.setBearing(30);">30</button><button onclick="L.Rotate._map.setBearing(45);" title="map.setBearing(45);">45</button><button onclick="L.Rotate._map.setBearing(60);" title="map.setBearing(60);">60</button><button onclick="L.Rotate._map.setBearing(75);" title="map.setBearing(75);">75</button><button onclick="L.Rotate._map.setBearing(90);" title="map.setBearing(90);">90</button><button onclick="L.Rotate._map.setBearing(180);" title="map.setBearing(180);">180</button></td><td><input title="increase/decrease rotation angle" type="range" min="0" max="360" step="1" value="0" name="rho" id='rho_input' /><span id='rho'></span></td><td><input title="increase/decrease padding bounds" type="range" min="-0.25" max="0" value="0" step="0.01" name="pad" id='pad_input' /></td><td><button onclick="L.Rotate.randomRotateMarkers(L.Rotate._map);" title="markers.setRotation(Math.random() * Math.PI / 30 + 0.1)">Rotate Markers</button><button onclick="L.Rotate.randomMarkers(L.Rotate._map, 100);" title="add 100 random markers to map">Add Markers</button></td></tr><tr><th>LatLng </th><td id='llx'></td><td id='lly'></td><td class='long'>LatLng of mouse pointer</td></tr><tr><th>Rel to pane </th><td id='lyx'></td><td id='lyy'></td><td class='long'>Mouse pointer pixel coords relative to _rotatePane</td></tr><tr><th>Rel to container</th><td id='cnx'></td><td id='cny'></td><td class='long'>Mouse pointer coords relative to map &lt;div&gt;</td></tr><tr><th>Pivot </th><td id='pvx'></td><td id='pvy'></td><td class='long'>Last _rotationPane pivot pixel coords relative to _rotationPane</td></tr><tr><th>Pane offset </th><td id='pox'></td><td id='poy'></td><td class='long'>Pixel offset of _mapPane</td></tr><tr><th>Pixel origin </th><td id='ogx'></td><td id='ogy'></td><td class='long'>Negative pixel coords of the (0,0) CRS point relative to _rotatePane</td></tr><tr><th>Pixel bounds </th><td id='ogbx'></td><td id='ogby'></td><td class='long'>Bounds of the current map view in projected pixel coordinates {min, max}.</td></tr></table>`),t.___rotatePixelOrigin=e.DomUtil.create("div","pixelorigin",t._rotatePane),t.___rotatePivotDot=e.DomUtil.create("div","pivot",t._mapPane),this.___rotateBounds=e.DomUtil.create("div","panebounds rotate",t._rotatePane),this.___mainBounds=e.DomUtil.create("div","panebounds main",t._mapPane),this.updatePaneBounds();const i=document.getElementById("rho_input"),o=document.getElementById("pad_input");if(i.addEventListener("change",e.Rotate.rotate.bind(this)),i.addEventListener("mousemove",e.Rotate.rotate.bind(this)),o.addEventListener("change",e.Rotate.displayCenter.bind(this)),o.addEventListener("mousemove",e.Rotate.displayCenter.bind(this)),o.addEventListener("change",e.Rotate.displayBounds.bind(this)),o.addEventListener("mousemove",e.Rotate.displayBounds.bind(this)),o.addEventListener("change",e.Rotate.displayLayersBounds.bind(this)),o.addEventListener("mousemove",e.Rotate.displayLayersBounds.bind(this)),o.addEventListener("change",e.Rotate.displayPixelBounds.bind(this)),o.addEventListener("mousemove",e.Rotate.displayPixelBounds.bind(this)),t.on("move rotate zoomend",this.updatePanePos),t.on("resize",this.updatePaneBounds.bind(this)),t.on("rotate zoomend load",this.updatePixelOrigin),t.on("rotate",this.updateRhoInput),t.on("rotate",this.updatePivotDot),t.on("moveend zoomend resetview rotate",this.displayCenter.bind(this)),t.on("moveend zoomend resetview rotate",this.displayBounds.bind(this)),t.on("moveend zoomend resetview rotate",this.displayLayersBounds.bind(this)),t.on("moveend zoomend resetview rotate",this.displayPixelBounds.bind(this)),this.updatePixelOrigin.call(t),this.updateRhoInput.call(t),this.updatePivotDot.call(t),this.displayCenter.call(this),this.displayBounds.call(this),this.displayLayersBounds.call(this),this.displayPixelBounds.call(this),e.Browser.mobile&&t.compassBearing.enable(),e.version.startsWith("2.")){const a=t.getContainer();a.addEventListener("mousemove",function(r){const s=a.getBoundingClientRect(),d=new e.Point(r.clientX-s.left-a.clientLeft,r.clientY-s.top-a.clientTop);e.Rotate.logMouse.call(t,{latlng:t.containerPointToLatLng(d),layerPoint:t.containerPointToLayerPoint(d),containerPoint:d})})}else t.on("mousemove",this.logMouse);t.on("locationfound",this.resetHeading),n.log&&t.on("click movestart move moveend zoomstart zoomend locationfound locationerror locationfound",this.logEvent)},logEvent(t){console.log(t,t.type)},logMouse(t){document.getElementById("llx").innerHTML=t.latlng.lng,document.getElementById("lly").innerHTML=t.latlng.lat,document.getElementById("lyx").innerHTML=t.layerPoint.x,document.getElementById("lyy").innerHTML=t.layerPoint.y,document.getElementById("cnx").innerHTML=t.containerPoint.x,document.getElementById("cny").innerHTML=t.containerPoint.y},rotate(t){return(t.type!=="mousemove"||t.buttons!==0)&&this._map.setBearing(t.target.valueAsNumber)},updatePaneBounds(){const t=this._map.getSize();for(const n of[this.___rotateBounds,this.___mainBounds])n.style.width=t.x+"px",n.style.height=t.y+"px"},updatePixelOrigin(){this._pixelOrigin&&(document.getElementById("ogx").innerHTML=this._pixelOrigin.x,document.getElementById("ogy").innerHTML=this._pixelOrigin.y,this.___rotatePixelOrigin.style.left=-this._pixelOrigin.x-1+"px",this.___rotatePixelOrigin.style.top=-this._pixelOrigin.y-1+"px")},updatePivotDot(){this._pivot&&(document.getElementById("pvx").innerHTML=this._pivot.x,document.getElementById("pvy").innerHTML=this._pivot.y,this.___rotatePivotDot.style.left=this._pivot.x-1+"px",this.___rotatePivotDot.style.top=this._pivot.y-1+"px")},updatePanePos(){const t=this._getMapPanePos();document.getElementById("pox").innerHTML=t.x,document.getElementById("poy").innerHTML=t.y},updateRhoInput(){document.getElementById("rho_input").value=this.getBearing()},rotateOneDegree(){const t=document.getElementById("rho_input").valueAsNumber++;this.setBearing(t<360?t:0)},randomRotateMarkers(t){const n=[],i=[];for(const a in t._layers)t._layers[a]instanceof e.Marker&&(n.push(t._layers[a]),i.push(Math.random()*Math.PI/30+.1));function o(a){for(let r=0;r<n.length;r++)n[r].setRotation(n[r].options.rotation+i[r]);--a&&setTimeout(o.bind(null,a),20)}o(25)},randomMarkers(t,n){t=this._map;const i=function(){for(let o=0;o<n;o++){const a=t.getBounds(),r=a.getSouthWest(),s=a.getNorthEast(),d=s.lng-r.lng,l=s.lat-r.lat,c=new e.Marker(new e.LatLng(r.lat+l*Math.random(),r.lng+d*Math.random()));e.Rotate.___cluster.addLayer(c)}};this.___cluster?typeof e.markerClusterGroup=="function"&&i():this.___cluster=import("https://unpkg.com/leaflet.markercluster@1.4.1/src/index.js").then(o=>{document.head.insertAdjacentHTML("beforeend",'<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"><link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css">'),Object.assign(globalThis.L,o),this.___cluster=e.markerClusterGroup({spiderfyOnMaxZoom:!1,showCoverageOnHover:!1,zoomToBoundsOnClick:!1}).on("clusterclick",a=>a.layer.spiderfy()).addTo(t),i()})},displayCenter(){const t=this._map;this.___centerMarker&&this.___centerMarker.remove(),this.___centerMarker=new e.CircleMarker(t.getCenter(),{radius:3,color:"red"}).addTo(t).bindTooltip("<b>center</b>",{direction:"right",permanent:!0}),this.___centerMarker.getTooltip().getElement().setAttribute("tabindex","-1")},displayBounds(){const t=this._map;this.___bounds&&this.___bounds.remove();const n=document.getElementById("pad_input").valueAsNumber,i=t.getBounds().pad(n);this.___bounds=new e.Rectangle(i,{fill:!1,lineCap:"square",color:"yellow",dashArray:"5, 10",weight:2}).addTo(t)},displayLayersBounds(){for(const t of["path","polygon","circle"]){if(globalThis[t])this[`___${t}bounds`]&&this[`___${t}bounds`].remove();else continue;const i=globalThis[t].getBounds();this[`___${t}bounds`]=new e.FeatureGroup([new e.Rectangle(i,{fill:!1,lineCap:"square",color:"yellow",dashArray:"5, 10",weight:1,opacity:.5}),new e.Marker(i.getNorthWest(),{scale:.5,rotation:-45*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>NW</b>",{direction:"center"}),new e.Marker(i.getNorthEast(),{scale:.5,rotation:45*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>NE</b>",{direction:"center"}),new e.Marker(i.getSouthEast(),{scale:.5,rotation:135*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>SE</b>",{direction:"center"}),new e.Marker(i.getSouthWest(),{scale:.5,rotation:-135*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>SW</b>",{direction:"center"})]).addTo(map).eachLayer(o=>{o.getElement&&o.getElement()&&(o.getElement().style.filter="hue-rotate(150deg)")})}},displayPixelBounds(){const t=this._map;this.___corners&&this.___corners.remove(),this.___pixelbounds&&this.___pixelbounds.remove();const n=t.getSize(),i=t.getPixelBounds(),o=new e.Point(0,0),a=new e.Point(n.x,0),r=new e.Point(n.x,n.y),s=new e.Point(0,n.y),d=new e.Marker(t.containerPointToLatLng(o),{title:"Top Left corner",rotation:-45*(Math.PI/180)}),l=new e.Marker(t.containerPointToLatLng(a),{title:"Top Right corner",rotation:45*(Math.PI/180)}),c=new e.Marker(t.containerPointToLatLng(r),{title:"Bottom Right corner",rotation:135*(Math.PI/180)}),h=new e.Marker(t.containerPointToLatLng(s),{title:"Bottom Left corner",rotation:-135*(Math.PI/180)});this.___corners=new e.FeatureGroup([d,l,c,h]).addTo(t).eachLayer(u=>u.getElement().style.filter="hue-rotate(150deg)"),document.getElementById("ogbx").innerHTML=JSON.stringify(i.min),document.getElementById("ogby").innerHTML=JSON.stringify(i.max)},resetHeading(t){t.heading!==null&&this.setBearing(t.heading)},locate(){this._map.locate({setView:!0,enableHighAccuracy:!0})}}})();
1
+ (function(){const e=Object.isExtensible(globalThis.L)?globalThis.L:Object.assign({},globalThis.L);globalThis.L=e,e.Rotate={debug(t,o={}){this._map=t,document.head.insertAdjacentHTML("beforeend",'<style>.leaflet-rotate-tools{display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;margin-bottom:6px}.leaflet-rotate-tools input,.leaflet-rotate-tools button{vertical-align:middle}.leaflet-rotate-info{border-collapse:collapse}.leaflet-rotate-info td,.leaflet-rotate-info th{padding:1px 12px 1px 0;text-align:left;white-space:nowrap;vertical-align:top}.leaflet-rotate-info td:nth-of-type(1),.leaflet-rotate-info td:nth-of-type(2){font-family:monospace;min-width:12ch}.leaflet-rotate-info tr[title]{cursor:help}div.crosshair{position:absolute;width:1px;height:1px;border:1px solid green;z-index:100;left:50%;right:50%}div.pivot,div.pixelorigin{position:absolute;width:1px;height:1px;border:5px solid yellow;border-radius:100%;z-index:1000}div.pivot:before{left:-1.5em;bottom:-2em;position:absolute;content:"PIVOT";color:#ff0}div.pixelorigin:before{left:-1.75em;top:-2em;position:absolute;content:"ORIGIN";color:#ff0}div.panebounds.main{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid red;border-bottom:1px red dashed;border-right:1px red dashed;z-index:100}div.panebounds.rotate{position:absolute;width:800px;height:600px;box-sizing:border-box;border:1px solid purple;border-bottom:1px purple dashed;border-right:1px purple dashed;z-index:100}div.crosshair.c1{left:calc(50% - 5px);top:50%;height:0;width:10px}div.crosshair.c2{left:50%;top:calc(50% - 5px);width:0;height:10px}#rho_input{vertical-align:middle}.panebounds:before{padding:1ch}.panebounds.rotate:before{float:right;content:"ROTATED"}.panebounds.main:before{float:left;content:"MAIN"}</style>'),t._container.insertAdjacentHTML("afterbegin",'<div class="crosshair c1"></div><div class="crosshair c2"></div>');const i=`
2
+ <div class="leaflet-rotate-tools">
3
+ <span>Bearing
4
+ ${[0,15,30,45,60,75,90,180].map(n=>`<button onclick="L.Rotate._map.setBearing(${n});" title="map.setBearing(${n});">${n}</button>`).join("")}
5
+ <input title="increase/decrease rotation angle" type="range" min="0" max="360" step="1" value="0" name="rho" id='rho_input' /><span id='rho'></span>
6
+ </span>
7
+ <label>Bounds padding <input title="increase/decrease padding bounds" type="range" min="-0.25" max="0" value="0" step="0.01" name="pad" id='pad_input' /></label>
8
+ <span>Markers
9
+ <button onclick="L.Rotate.randomRotateMarkers(L.Rotate._map);" title="markers.setRotation(Math.random() * Math.PI / 30 + 0.1)">Rotate</button>
10
+ <button onclick="L.Rotate.randomMarkers(L.Rotate._map, 100);" title="add 100 random markers to map">Add 100</button>
11
+ </span>
12
+ </div>
13
+ <table class="leaflet-rotate-info">
14
+ <tr><th></th><th>x / lng</th><th>y / lat</th></tr>
15
+ ${[["LatLng","LatLng of mouse pointer","llx","lly"],["Rel to pane","Mouse pointer pixel coords relative to _rotatePane","lyx","lyy"],["Rel to container","Mouse pointer coords relative to map <div>","cnx","cny"],["Pivot","Last _rotationPane pivot pixel coords relative to _rotationPane","pvx","pvy"],["Pane offset","Pixel offset of _mapPane","pox","poy"],["Pixel origin","Negative pixel coords of the (0,0) CRS point relative to _rotatePane","ogx","ogy"],["Pixel bounds (min / max)","Bounds of the current map view in projected pixel coordinates {min, max}","ogbx","ogby"]].map(([n,s,d,l])=>`<tr title="${s.replace("<","&lt;").replace(">","&gt;")}"><th>${n}</th><td id='${d}'></td><td id='${l}'></td></tr>`).join("")}
16
+ </table>`;o.container?o.container.insertAdjacentHTML("beforeend",`<details open><summary>Debug</summary>${i}</details>`):t._container.insertAdjacentHTML("afterend","<hr>"+i),t.___rotatePixelOrigin=e.DomUtil.create("div","pixelorigin",t._rotatePane),t.___rotatePivotDot=e.DomUtil.create("div","pivot",t._mapPane),this.___rotateBounds=e.DomUtil.create("div","panebounds rotate",t._rotatePane),this.___mainBounds=e.DomUtil.create("div","panebounds main",t._mapPane),this.updatePaneBounds();const r=document.getElementById("rho_input"),a=document.getElementById("pad_input");if(r.addEventListener("change",e.Rotate.rotate.bind(this)),r.addEventListener("mousemove",e.Rotate.rotate.bind(this)),a.addEventListener("change",e.Rotate.displayCenter.bind(this)),a.addEventListener("mousemove",e.Rotate.displayCenter.bind(this)),a.addEventListener("change",e.Rotate.displayBounds.bind(this)),a.addEventListener("mousemove",e.Rotate.displayBounds.bind(this)),a.addEventListener("change",e.Rotate.displayLayersBounds.bind(this)),a.addEventListener("mousemove",e.Rotate.displayLayersBounds.bind(this)),a.addEventListener("change",e.Rotate.displayPixelBounds.bind(this)),a.addEventListener("mousemove",e.Rotate.displayPixelBounds.bind(this)),t.on("move rotate zoomend",this.updatePanePos),t.on("resize",this.updatePaneBounds.bind(this)),t.on("rotate zoomend load",this.updatePixelOrigin),t.on("rotate",this.updateRhoInput),t.on("rotate",this.updatePivotDot),t.on("moveend zoomend resetview rotate",this.displayCenter.bind(this)),t.on("moveend zoomend resetview rotate",this.displayBounds.bind(this)),t.on("moveend zoomend resetview rotate",this.displayLayersBounds.bind(this)),t.on("moveend zoomend resetview rotate",this.displayPixelBounds.bind(this)),this.updatePixelOrigin.call(t),this.updateRhoInput.call(t),this.updatePivotDot.call(t),this.displayCenter.call(this),this.displayBounds.call(this),this.displayLayersBounds.call(this),this.displayPixelBounds.call(this),e.Browser.mobile&&t.compassBearing.enable(),e.version.startsWith("2.")){const n=t.getContainer();n.addEventListener("mousemove",function(s){const d=n.getBoundingClientRect(),l=new e.Point(s.clientX-d.left-n.clientLeft,s.clientY-d.top-n.clientTop);e.Rotate.logMouse.call(t,{latlng:t.containerPointToLatLng(l),layerPoint:t.containerPointToLayerPoint(l),containerPoint:l})})}else t.on("mousemove",this.logMouse);t.on("locationfound",this.resetHeading),o.log&&t.on("click movestart move moveend zoomstart zoomend locationfound locationerror locationfound",this.logEvent)},logEvent(t){console.log(t,t.type)},logMouse(t){const o=(i,r)=>+i.toFixed(r);document.getElementById("llx").innerHTML=o(t.latlng.lng,6),document.getElementById("lly").innerHTML=o(t.latlng.lat,6),document.getElementById("lyx").innerHTML=o(t.layerPoint.x,1),document.getElementById("lyy").innerHTML=o(t.layerPoint.y,1),document.getElementById("cnx").innerHTML=o(t.containerPoint.x,1),document.getElementById("cny").innerHTML=o(t.containerPoint.y,1)},rotate(t){return(t.type!=="mousemove"||t.buttons!==0)&&this._map.setBearing(t.target.valueAsNumber)},updatePaneBounds(){const t=this._map.getSize();for(const o of[this.___rotateBounds,this.___mainBounds])o.style.width=t.x+"px",o.style.height=t.y+"px"},updatePixelOrigin(){this._pixelOrigin&&(document.getElementById("ogx").innerHTML=+this._pixelOrigin.x.toFixed(1),document.getElementById("ogy").innerHTML=+this._pixelOrigin.y.toFixed(1),this.___rotatePixelOrigin.style.left=-this._pixelOrigin.x-1+"px",this.___rotatePixelOrigin.style.top=-this._pixelOrigin.y-1+"px")},updatePivotDot(){this._pivot&&(document.getElementById("pvx").innerHTML=this._pivot.x,document.getElementById("pvy").innerHTML=this._pivot.y,this.___rotatePivotDot.style.left=this._pivot.x-1+"px",this.___rotatePivotDot.style.top=this._pivot.y-1+"px")},updatePanePos(){const t=this._getMapPanePos();document.getElementById("pox").innerHTML=t.x,document.getElementById("poy").innerHTML=t.y},updateRhoInput(){document.getElementById("rho_input").value=this.getBearing()},rotateOneDegree(){const t=document.getElementById("rho_input").valueAsNumber++;this.setBearing(t<360?t:0)},randomRotateMarkers(t){const o=[],i=[];for(const a in t._layers)t._layers[a]instanceof e.Marker&&(o.push(t._layers[a]),i.push(Math.random()*Math.PI/30+.1));function r(a){for(let n=0;n<o.length;n++)o[n].setRotation(o[n].options.rotation+i[n]);--a&&setTimeout(r.bind(null,a),20)}r(25)},randomMarkers(t,o){t=this._map;const i=function(){for(let r=0;r<o;r++){const a=t.getBounds(),n=a.getSouthWest(),s=a.getNorthEast(),d=s.lng-n.lng,l=s.lat-n.lat,h=new e.Marker(new e.LatLng(n.lat+l*Math.random(),n.lng+d*Math.random()));e.Rotate.___cluster.addLayer(h)}};this.___cluster?typeof e.markerClusterGroup=="function"&&i():this.___cluster=import("https://unpkg.com/leaflet.markercluster@1.4.1/src/index.js").then(r=>{document.head.insertAdjacentHTML("beforeend",'<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"><link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css">'),Object.assign(globalThis.L,r),this.___cluster=e.markerClusterGroup({spiderfyOnMaxZoom:!1,showCoverageOnHover:!1,zoomToBoundsOnClick:!1}).on("clusterclick",a=>a.layer.spiderfy()).addTo(t),i()})},displayCenter(){const t=this._map;this.___centerMarker&&this.___centerMarker.remove(),this.___centerMarker=new e.CircleMarker(t.getCenter(),{radius:3,color:"red"}).addTo(t).bindTooltip("<b>center</b>",{direction:"right",permanent:!0}),this.___centerMarker.getTooltip().getElement().setAttribute("tabindex","-1")},displayBounds(){const t=this._map;this.___bounds&&this.___bounds.remove();const o=document.getElementById("pad_input").valueAsNumber,i=t.getBounds().pad(o);this.___bounds=new e.Rectangle(i,{fill:!1,lineCap:"square",color:"yellow",dashArray:"5, 10",weight:2}).addTo(t)},displayLayersBounds(){for(const t of["path","polygon","circle"]){if(globalThis[t])this[`___${t}bounds`]&&this[`___${t}bounds`].remove();else continue;const i=globalThis[t].getBounds();this[`___${t}bounds`]=new e.FeatureGroup([new e.Rectangle(i,{fill:!1,lineCap:"square",color:"yellow",dashArray:"5, 10",weight:1,opacity:.5}),new e.Marker(i.getNorthWest(),{scale:.5,rotation:-45*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>NW</b>",{direction:"center"}),new e.Marker(i.getNorthEast(),{scale:.5,rotation:45*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>NE</b>",{direction:"center"}),new e.Marker(i.getSouthEast(),{scale:.5,rotation:135*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>SE</b>",{direction:"center"}),new e.Marker(i.getSouthWest(),{scale:.5,rotation:-135*(Math.PI/180),rotateWithView:!0}).bindTooltip("<b>SW</b>",{direction:"center"})]).addTo(map).eachLayer(r=>{r.getElement&&r.getElement()&&(r.getElement().style.filter="hue-rotate(150deg)")})}},displayPixelBounds(){const t=this._map;this.___corners&&this.___corners.remove(),this.___pixelbounds&&this.___pixelbounds.remove();const o=t.getSize(),i=t.getPixelBounds(),r=new e.Point(0,0),a=new e.Point(o.x,0),n=new e.Point(o.x,o.y),s=new e.Point(0,o.y),d=new e.Marker(t.containerPointToLatLng(r),{title:"Top Left corner",rotation:-45*(Math.PI/180)}),l=new e.Marker(t.containerPointToLatLng(a),{title:"Top Right corner",rotation:45*(Math.PI/180)}),h=new e.Marker(t.containerPointToLatLng(n),{title:"Bottom Right corner",rotation:135*(Math.PI/180)}),u=new e.Marker(t.containerPointToLatLng(s),{title:"Bottom Left corner",rotation:-135*(Math.PI/180)});this.___corners=new e.FeatureGroup([d,l,h,u]).addTo(t).eachLayer(c=>c.getElement().style.filter="hue-rotate(150deg)");const p=c=>`${+c.x.toFixed(1)}, ${+c.y.toFixed(1)}`;document.getElementById("ogbx").innerHTML=p(i.min),document.getElementById("ogby").innerHTML=p(i.max)},resetHeading(t){t.heading!==null&&this.setBearing(t.heading)},locate(){this._map.locate({setView:!0,enableHighAccuracy:!0})}}})();
@@ -1,3 +1,10 @@
1
+ /*
2
+ * For Leaflet.heat (https://github.com/Leaflet/Leaflet.heat) or the heatmap.js
3
+ * Leaflet plugin (https://github.com/pa7/heatmap.js/tree/master/plugins/leaflet-heatmap),
4
+ * load this file after the heatmap plugin. It adapts heat redraw coordinates to
5
+ * the rotated pane.
6
+ */
7
+
1
8
  if (!L.HeatLayer && !globalThis.HeatmapOverlay) {
2
9
  throw new Error('Load Leaflet.heat or heatmap.js Leaflet plugin before this adapter.');
3
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaflet-rotate",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "A Leaflet plugin that allows to add rotation functionality to map tiles",
5
5
  "type": "module",
6
6
  "main": "dist/leaflet-rotate.js",
@@ -1,168 +1,224 @@
1
1
  /**
2
- * A tri-state control for map rotation, states are:
2
+ * Leaflet control showing the map bearing and pitch, MapLibre-like (`NavigationControl` compass):
3
3
  *
4
- * - Locked (default)
5
- * - Unlocked (user can pinch-rotate)
6
- * - Follow (rotation follows device orientation, if available)
4
+ * - Click: animate back to north (and to zero pitch when `visualizePitch` is enabled).
5
+ * - Drag: horizontal movement changes the bearing, vertical movement changes the pitch.
7
6
  *
8
- * @typedef L.Control.Rotate
7
+ * Touch rotation and compass bearing are configured with the `touchRotate` and `compassBearing`
8
+ * map handlers (eg. `map.compassBearing.enable()`).
9
+ *
10
+ * @class L.Control.Rotate
11
+ *
12
+ * @extends L.Control
13
+ *
14
+ * @property {string} [position='topleft'] Leaflet control position.
15
+ * @property {boolean} [closeOnZeroBearing=true] Hide the control when both bearing and pitch are zero.
16
+ * @property {boolean} [visualizePitch=true] Tilt the icon with the pitch, reset the pitch on click and change it on drag.
17
+ *
18
+ * @since leaflet-rotate (v0.1.0)
9
19
  */
10
20
 
11
21
  L.Control.Rotate = L.Control.extend({
12
22
 
13
23
  options: {
14
24
  position: 'topleft',
15
- closeOnZeroBearing: true
25
+ closeOnZeroBearing: true,
26
+ visualizePitch: true,
16
27
  },
17
28
 
29
+ /**
30
+ * Create the control element and subscribe to map rotation and pitch.
31
+ *
32
+ * @param {L.Map} map
33
+ *
34
+ * @returns {HTMLElement} Control container.
35
+ *
36
+ * @since leaflet-rotate (v0.1.0)
37
+ */
18
38
  onAdd(map) {
19
39
  const container = this._container = L.DomUtil.create('div', 'leaflet-control-rotate leaflet-bar');
20
40
 
21
- // this.button = L.Control.Zoom.prototype._createButton.call(this, 'R', 'leaflet-control-rotate', 'leaflet-control-rotate', container, this._toggleLock);
22
-
23
41
  const arrow = this._arrow = L.DomUtil.create('span', 'leaflet-control-rotate-arrow');
24
42
 
25
43
  arrow.style.backgroundImage = `url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10.5 14l4-8 4 8h-8z'/%3E%3Cpath d='M10.5 16l4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E")`;
26
- arrow.style.cursor = 'grab';
27
44
  arrow.style.display = 'block';
28
45
  arrow.style.width = '100%';
29
46
  arrow.style.height = '100%';
30
47
  arrow.style.backgroundRepeat = 'no-repeat';
31
48
  arrow.style.backgroundPosition = '50%';
49
+ arrow.style.pointerEvents = 'none';
32
50
 
33
- // Copy-pasted from L.Control.Zoom
34
51
  const link = this._link = L.DomUtil.create('a', 'leaflet-control-rotate-toggle', container);
35
52
  link.appendChild(arrow);
36
53
  link.href = '#';
37
- link.title = 'Rotate map';
54
+ link.title = this.options.visualizePitch ? 'Reset bearing and pitch' : 'Reset bearing to north';
55
+ link.setAttribute('role', 'button');
56
+ link.setAttribute('aria-label', link.title);
57
+ link.style.cursor = 'grab';
58
+ link.style.touchAction = 'none';
59
+
60
+ this._onPointerMoveBound = this._onPointerMove.bind(this);
61
+ this._onPointerUpBound = this._onPointerUp.bind(this);
38
62
 
39
63
  L.DomEvent
40
- .on(link, 'dblclick', L.DomEvent.stopPropagation)
41
- .on(link, 'mousedown', this._handleMouseDown, this)
64
+ .on(link, 'dblclick contextmenu', L.DomEvent.stop)
42
65
  .on(link, 'click', L.DomEvent.stop)
43
- .on(link, 'click', this._cycleState, this)
66
+ .on(link, 'click', this._onClick, this)
44
67
  .on(link, 'click', this._refocusOnMap, this);
68
+ link.addEventListener('pointerdown', this._onPointerDown.bind(this));
45
69
 
70
+ map.on('rotate pitch', this._restyle, this);
71
+ this._map = map;
46
72
  this._restyle();
47
73
 
48
- map.on('rotate', this._restyle, this);
49
-
50
- // State flag
51
- this._follow = false;
52
- this._canFollow = false;
53
-
54
- if (this.options.closeOnZeroBearing && map.getBearing() === 0) {
55
- container.style.display = 'none';
56
- }
57
-
58
74
  return container;
59
75
  },
60
-
76
+
77
+ /**
78
+ * Remove the map event listener when the control is detached.
79
+ *
80
+ * @param {L.Map} map
81
+ *
82
+ * @since leaflet-rotate (v0.1.0)
83
+ */
61
84
  onRemove(map) {
62
- map.off('rotate', this._restyle, this);
85
+ this._onPointerUp();
86
+ map.off('rotate pitch', this._restyle, this);
63
87
  },
64
88
 
65
- _handleMouseDown(e) {
66
- L.DomEvent.stop(e);
67
- this.dragging = true;
68
- this.dragstartX = e.pageX;
69
- this.dragstartY = e.pageY;
70
- L.DomEvent
71
- .on(document, 'mousemove', this._handleMouseDrag, this)
72
- .on(document, 'mouseup', this._handleMouseUp, this);
89
+ /**
90
+ * Reset bearing (and pitch) unless the click ends a drag.
91
+ *
92
+ * @since leaflet-rotate (0.5.1)
93
+ */
94
+ _onClick() {
95
+ if (this._moved) {
96
+ this._moved = false;
97
+ return;
98
+ }
99
+ if (!this._map || !this._map._rotate) { return; }
100
+ if (this.options.visualizePitch) {
101
+ this._map.resetNorthPitch();
102
+ } else {
103
+ this._map.resetNorth();
104
+ }
73
105
  },
74
106
 
75
- _handleMouseUp(e) {
76
- L.DomEvent.stop(e);
77
- this.dragging = false;
78
-
79
- L.DomEvent
80
- .off(document, 'mousemove', this._handleMouseDrag, this)
81
- .off(document, 'mouseup', this._handleMouseUp, this);
107
+ /**
108
+ * Start dragging the control (mouse, pen or touch).
109
+ *
110
+ * @param {PointerEvent} e
111
+ *
112
+ * @since leaflet-rotate (v0.1.0)
113
+ */
114
+ _onPointerDown(e) {
115
+ if (!this._map || !this._map._rotate || e.button > 0 || this._pointerId != null) { return; }
116
+ e.preventDefault();
117
+ e.stopPropagation();
118
+ this._pointerId = e.pointerId;
119
+ this._startX = this._lastX = e.clientX;
120
+ this._startY = this._lastY = e.clientY;
121
+ this._moved = false;
122
+ this._link.style.cursor = 'grabbing';
123
+ document.addEventListener('pointermove', this._onPointerMoveBound);
124
+ document.addEventListener('pointerup', this._onPointerUpBound);
125
+ document.addEventListener('pointercancel', this._onPointerUpBound);
82
126
  },
83
127
 
84
- _handleMouseDrag(e) {
85
- if (!this.dragging) { return; }
86
- const deltaX = e.clientX - this.dragstartX;
87
- this._map.setBearing(deltaX);
128
+ /**
129
+ * Horizontal movement changes the bearing, vertical movement the pitch.
130
+ *
131
+ * @param {PointerEvent} e
132
+ *
133
+ * @since leaflet-rotate (v0.1.0)
134
+ */
135
+ _onPointerMove(e) {
136
+ if (e.pointerId !== this._pointerId) { return; }
137
+ e.preventDefault();
138
+ if (!this._moved && Math.hypot(e.clientX - this._startX, e.clientY - this._startY) < 3) { return; }
139
+ if (!this._moved) {
140
+ this._moved = true;
141
+ this._map._stopPitchBearingAnimation();
142
+ }
143
+ const map = this._map,
144
+ pitch = this.options.visualizePitch ? map.getPitch() + (this._lastY - e.clientY) * 0.5 : map.getPitch();
145
+ map._setPitchAndBearing(pitch, map.getBearing() + (e.clientX - this._lastX) * 0.8);
146
+ this._lastX = e.clientX;
147
+ this._lastY = e.clientY;
88
148
  },
89
149
 
90
- _cycleState(ev) {
91
- if (!this._map) {
92
- return;
150
+ /**
151
+ * Stop dragging and remove document listeners.
152
+ *
153
+ * @param {PointerEvent} [e]
154
+ *
155
+ * @since leaflet-rotate (v0.1.0)
156
+ */
157
+ _onPointerUp(e) {
158
+ if (e && e.pointerId !== this._pointerId) { return; }
159
+ document.removeEventListener('pointermove', this._onPointerMoveBound);
160
+ document.removeEventListener('pointerup', this._onPointerUpBound);
161
+ document.removeEventListener('pointercancel', this._onPointerUpBound);
162
+ // a drag ending outside the button produces no click
163
+ if (e && this._moved && !this._link.contains(e.target)) { this._moved = false; }
164
+ delete this._pointerId;
165
+ if (this._link) {
166
+ this._link.style.cursor = 'grab';
167
+ this._restyle();
93
168
  }
169
+ },
94
170
 
171
+ /**
172
+ * Update the icon (bearing and pitch) and the control visibility.
173
+ *
174
+ * @listens L.Map#rotate
175
+ * @listens L.Map#pitch
176
+ *
177
+ * @since leaflet-rotate (v0.1.0)
178
+ */
179
+ _restyle() {
95
180
  const map = this._map;
96
-
97
- // Touch mode
98
- if (!map.touchRotate.enabled() && !map.compassBearing.enabled()) {
99
- map.touchRotate.enable();
100
- }
101
-
102
- // Compass mode
103
- else if (!map.compassBearing.enabled()) {
104
- map.touchRotate.disable();
105
- (
106
- DeviceOrientationEvent && DeviceOrientationEvent.requestPermission
107
- ? DeviceOrientationEvent.requestPermission() // iOS compass
108
- : Promise.resolve('granted') // others
109
- ).then(state => "granted" === state && map.compassBearing.enable())
181
+ if (!map) { return; }
182
+ if (!map.options.rotate) {
183
+ this._link.classList.add('leaflet-disabled');
184
+ return;
110
185
  }
111
186
 
112
- // Locked mode
113
- else {
114
- map.compassBearing.disable();
115
- map.setBearing(0);
116
- if (this.options.closeOnZeroBearing) {
117
- map.touchRotate.enable();
118
- }
119
- }
120
- this._restyle();
121
- },
187
+ const bearing = map.getBearing(),
188
+ pitch = map.getPitch();
122
189
 
123
- _restyle() {
124
- if (!this._map.options.rotate) {
125
- this._link.classList.add('leaflet-disabled');
126
- } else {
127
- const map = this._map;
128
- const bearing = map.getBearing();
129
-
130
- this._arrow.style.transform = 'rotate(' + bearing + 'deg)';
131
-
132
- if (bearing && this.options.closeOnZeroBearing) {
133
- this._container.style.display = 'block';
134
- }
135
-
136
- // Compass mode
137
- if (map.compassBearing.enabled()) {
138
- this._link.style.backgroundColor = 'orange';
139
- }
140
-
141
- // Touch mode
142
- else if (map.touchRotate.enabled()) {
143
- this._link.style.backgroundColor = null;
144
- }
145
-
146
- // Locked mode
147
- else {
148
- this._link.style.backgroundColor = 'grey';
149
- if (0 === bearing && this.options.closeOnZeroBearing) {
150
- this._container.style.display = 'none';
151
- }
152
- }
190
+ this._arrow.style.transform = this.options.visualizePitch && pitch
191
+ ? `scale(${1 / Math.sqrt(Math.cos(pitch * Math.PI / 180))}) rotateX(${pitch}deg) rotate(${bearing}deg)`
192
+ : `rotate(${bearing}deg)`;
193
+
194
+ if (this.options.closeOnZeroBearing) {
195
+ const hidden = !bearing && !(this.options.visualizePitch && pitch) && this._pointerId == null;
196
+ this._container.style.display = hidden ? 'none' : 'block';
153
197
  }
154
198
  },
155
199
 
156
200
  });
157
-
158
201
  if (L.control) {
159
202
  L.control.rotate = options => new L.Control.Rotate(options);
160
203
  }
161
204
 
162
205
  L.Map.mergeOptions({
206
+ /**
207
+ * Add the rotation control automatically. Set to `false` to disable,
208
+ * or pass an options object to configure the control.
209
+ *
210
+ * @type {boolean|Object}
211
+ *
212
+ * @since leaflet-rotate (v0.1.0)
213
+ */
163
214
  rotateControl: true,
164
215
  });
165
216
 
217
+ /**
218
+ * Add the configured rotation control to each newly initialized map.
219
+ *
220
+ * @since leaflet-rotate (v0.1.0)
221
+ */
166
222
  L.Map.addInitHook(function() {
167
223
  if (this.options.rotateControl) {
168
224
  const options = typeof this.options.rotateControl === 'object' ? this.options.rotateControl : {};
@@ -170,3 +226,4 @@ L.Map.addInitHook(function() {
170
226
  this.addControl(this.rotateControl);
171
227
  }
172
228
  });
229
+