leaflet-rotate 0.5.0 → 0.5.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/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})}}})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leaflet-rotate",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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,17 +1,19 @@
1
1
  /**
2
- * Leaflet control for changing map rotation and selecting a rotation mode.
3
- * Its modes are:
2
+ * Leaflet control showing the map bearing and pitch, MapLibre-like (`NavigationControl` compass):
4
3
  *
5
- * - Locked: rotation gestures are disabled.
6
- * - Touch: two-finger rotation is enabled.
7
- * - Compass: bearing follows device orientation when supported and permitted.
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.
6
+ *
7
+ * Touch rotation and compass bearing are configured with the `touchRotate` and `compassBearing`
8
+ * map handlers (eg. `map.compassBearing.enable()`).
8
9
  *
9
10
  * @class L.Control.Rotate
10
11
  *
11
12
  * @extends L.Control
12
13
  *
13
14
  * @property {string} [position='topleft'] Leaflet control position.
14
- * @property {boolean} [closeOnZeroBearing=true] Hide the control when bearing is zero.
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.
15
17
  *
16
18
  * @since leaflet-rotate (v0.1.0)
17
19
  */
@@ -20,11 +22,12 @@ L.Control.Rotate = L.Control.extend({
20
22
 
21
23
  options: {
22
24
  position: 'topleft',
23
- closeOnZeroBearing: true
25
+ closeOnZeroBearing: true,
26
+ visualizePitch: true,
24
27
  },
25
28
 
26
29
  /**
27
- * Create the control element and subscribe to map rotation.
30
+ * Create the control element and subscribe to map rotation and pitch.
28
31
  *
29
32
  * @param {L.Map} map
30
33
  *
@@ -35,46 +38,42 @@ L.Control.Rotate = L.Control.extend({
35
38
  onAdd(map) {
36
39
  const container = this._container = L.DomUtil.create('div', 'leaflet-control-rotate leaflet-bar');
37
40
 
38
- // this.button = L.Control.Zoom.prototype._createButton.call(this, 'R', 'leaflet-control-rotate', 'leaflet-control-rotate', container, this._toggleLock);
39
-
40
41
  const arrow = this._arrow = L.DomUtil.create('span', 'leaflet-control-rotate-arrow');
41
42
 
42
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")`;
43
- arrow.style.cursor = 'grab';
44
44
  arrow.style.display = 'block';
45
45
  arrow.style.width = '100%';
46
46
  arrow.style.height = '100%';
47
47
  arrow.style.backgroundRepeat = 'no-repeat';
48
48
  arrow.style.backgroundPosition = '50%';
49
+ arrow.style.pointerEvents = 'none';
49
50
 
50
- // Copy-pasted from L.Control.Zoom
51
51
  const link = this._link = L.DomUtil.create('a', 'leaflet-control-rotate-toggle', container);
52
52
  link.appendChild(arrow);
53
53
  link.href = '#';
54
- 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);
55
62
 
56
63
  L.DomEvent
57
- .on(link, 'dblclick', L.DomEvent.stopPropagation)
58
- .on(link, 'mousedown', this._handleMouseDown, this)
64
+ .on(link, 'dblclick contextmenu', L.DomEvent.stop)
59
65
  .on(link, 'click', L.DomEvent.stop)
60
- .on(link, 'click', this._cycleState, this)
66
+ .on(link, 'click', this._onClick, this)
61
67
  .on(link, 'click', this._refocusOnMap, this);
68
+ link.addEventListener('pointerdown', this._onPointerDown.bind(this));
62
69
 
70
+ map.on('rotate pitch', this._restyle, this);
71
+ this._map = map;
63
72
  this._restyle();
64
73
 
65
- map.on('rotate', this._restyle, this);
66
-
67
- // State flag
68
- this._follow = false;
69
- this._canFollow = false;
70
-
71
- if (this.options.closeOnZeroBearing && map.getBearing() === 0) {
72
- container.style.display = 'none';
73
- }
74
-
75
74
  return container;
76
75
  },
77
-
76
+
78
77
  /**
79
78
  * Remove the map event listener when the control is detached.
80
79
  *
@@ -83,138 +82,122 @@ L.Control.Rotate = L.Control.extend({
83
82
  * @since leaflet-rotate (v0.1.0)
84
83
  */
85
84
  onRemove(map) {
86
- map.off('rotate', this._restyle, this);
85
+ this._onPointerUp();
86
+ map.off('rotate pitch', this._restyle, this);
87
87
  },
88
88
 
89
89
  /**
90
- * Begin dragging the control's bearing indicator.
91
- *
92
- * @param {MouseEvent} e Mouse-down event.
90
+ * Reset bearing (and pitch) unless the click ends a drag.
93
91
  *
94
- * @since leaflet-rotate (v0.1.0)
92
+ * @since leaflet-rotate (0.5.1)
95
93
  */
96
- _handleMouseDown(e) {
97
- L.DomEvent.stop(e);
98
- this.dragging = true;
99
- this.dragstartX = e.pageX;
100
- this.dragstartY = e.pageY;
101
- L.DomEvent
102
- .on(document, 'mousemove', this._handleMouseDrag, this)
103
- .on(document, 'mouseup', this._handleMouseUp, this);
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
+ }
104
105
  },
105
106
 
106
107
  /**
107
- * End bearing-indicator dragging and remove document listeners.
108
+ * Start dragging the control (mouse, pen or touch).
108
109
  *
109
- * @param {MouseEvent} e Mouse-up event.
110
+ * @param {PointerEvent} e
110
111
  *
111
112
  * @since leaflet-rotate (v0.1.0)
112
113
  */
113
- _handleMouseUp(e) {
114
- L.DomEvent.stop(e);
115
- this.dragging = false;
116
-
117
- L.DomEvent
118
- .off(document, 'mousemove', this._handleMouseDrag, this)
119
- .off(document, 'mouseup', this._handleMouseUp, this);
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);
120
126
  },
121
127
 
122
128
  /**
123
- * Set bearing from the horizontal drag distance.
129
+ * Horizontal movement changes the bearing, vertical movement the pitch.
124
130
  *
125
- * @param {MouseEvent} e Mouse-move event.
131
+ * @param {PointerEvent} e
126
132
  *
127
133
  * @since leaflet-rotate (v0.1.0)
128
134
  */
129
- _handleMouseDrag(e) {
130
- if (!this.dragging) { return; }
131
- const deltaX = e.clientX - this.dragstartX;
132
- this._map.setBearing(deltaX);
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;
133
148
  },
134
149
 
135
150
  /**
136
- * Cycle through touch, compass, and locked rotation modes.
137
- * Compass mode may require permission from the browser.
151
+ * Stop dragging and remove document listeners.
138
152
  *
139
- * @param {Event} ev Control click event.
153
+ * @param {PointerEvent} [e]
140
154
  *
141
155
  * @since leaflet-rotate (v0.1.0)
142
156
  */
143
- _cycleState(ev) {
144
- if (!this._map) {
145
- return;
146
- }
147
-
148
- const map = this._map;
149
-
150
- // Touch mode
151
- if (!map.touchRotate.enabled() && !map.compassBearing.enabled()) {
152
- map.touchRotate.enable();
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();
153
168
  }
154
-
155
- // Compass mode
156
- else if (!map.compassBearing.enabled()) {
157
- map.touchRotate.disable();
158
- (
159
- DeviceOrientationEvent && DeviceOrientationEvent.requestPermission
160
- ? DeviceOrientationEvent.requestPermission() // iOS compass
161
- : Promise.resolve('granted') // others
162
- ).then(state => "granted" === state && map.compassBearing.enable())
163
- }
164
-
165
- // Locked mode
166
- else {
167
- map.compassBearing.disable();
168
- map.setBearing(0);
169
- if (this.options.closeOnZeroBearing) {
170
- map.touchRotate.enable();
171
- }
172
- }
173
- this._restyle();
174
169
  },
175
170
 
176
171
  /**
177
- * Update the control icon and visibility to match the current mode.
172
+ * Update the icon (bearing and pitch) and the control visibility.
178
173
  *
179
174
  * @listens L.Map#rotate
175
+ * @listens L.Map#pitch
180
176
  *
181
177
  * @since leaflet-rotate (v0.1.0)
182
178
  */
183
179
  _restyle() {
184
- if (!this._map.options.rotate) {
180
+ const map = this._map;
181
+ if (!map) { return; }
182
+ if (!map.options.rotate) {
185
183
  this._link.classList.add('leaflet-disabled');
186
- } else {
187
- const map = this._map;
188
- const bearing = map.getBearing();
189
-
190
- this._arrow.style.transform = 'rotate(' + bearing + 'deg)';
191
-
192
- if (bearing && this.options.closeOnZeroBearing) {
193
- this._container.style.display = 'block';
194
- }
195
-
196
- // Compass mode
197
- if (map.compassBearing.enabled()) {
198
- this._link.style.backgroundColor = 'orange';
199
- }
200
-
201
- // Touch mode
202
- else if (map.touchRotate.enabled()) {
203
- this._link.style.backgroundColor = null;
204
- }
205
-
206
- // Locked mode
207
- else {
208
- this._link.style.backgroundColor = 'grey';
209
- if (0 === bearing && this.options.closeOnZeroBearing) {
210
- this._container.style.display = 'none';
211
- }
212
- }
184
+ return;
185
+ }
186
+
187
+ const bearing = map.getBearing(),
188
+ pitch = map.getPitch();
189
+
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';
213
197
  }
214
198
  },
215
199
 
216
200
  });
217
-
218
201
  if (L.control) {
219
202
  L.control.rotate = options => new L.Control.Rotate(options);
220
203
  }
@@ -243,3 +226,4 @@ L.Map.addInitHook(function() {
243
226
  this.addControl(this.rotateControl);
244
227
  }
245
228
  });
229
+
package/src/index.js CHANGED
@@ -55,6 +55,7 @@ export * from './map/handler/CompassBearing';
55
55
  export * from './map/handler/ContainerMutation';
56
56
  export * from './map/handler/TouchGestures';
57
57
  export * from './map/handler/TouchRotate';
58
+ export * from './map/handler/TouchPitch';
58
59
  export * from './map/handler/ShiftKeyRotate';
59
60
  export * from './map/handler/TouchZoom';
60
61
 
@@ -27,11 +27,30 @@ L.GridLayer.include({
27
27
  if (!this._onRotate) {
28
28
  this._onRotate = L.Util.throttle(this._onMoveEnd, this.options.updateInterval, this);
29
29
  }
30
- events.rotate = this._onRotate;
30
+ events.rotate = this._onRotateEvent;
31
31
  }
32
32
  return events;
33
33
  },
34
34
 
35
+ /**
36
+ * Keep tile levels aligned when a bearing/pitch change shifts the pixel origin
37
+ * (eg. when the rotation pivot is not the view center), then refresh tiles (throttled).
38
+ *
39
+ * @listens L.Map~rotate
40
+ *
41
+ * @since leaflet-rotate (0.5.1)
42
+ */
43
+ _onRotateEvent() {
44
+ const map = this._map;
45
+ if (!map) { return; }
46
+ const origin = map.getPixelOrigin();
47
+ if (this._rotateOrigin && !this._rotateOrigin.equals(origin) && !map._animatingZoom && this._levels) {
48
+ this._setZoomTransforms(map.getCenter(), map.getZoom());
49
+ }
50
+ this._rotateOrigin = origin;
51
+ this._onRotate();
52
+ },
53
+
35
54
  /**
36
55
  * Return tile pixel bounds that cover the rotated viewport.
37
56
  *