index.js (9298B)
1 import L from 'leaflet'; 2 import ko from 'knockout'; 3 import {makeButtonWithBar} from '~/lib/leaflet.control.commons'; 4 import layout from './control.html'; 5 import '~/lib/controls-styles/controls-styles.css'; 6 import './style.css'; 7 import {getDeclination} from '~/lib/magnetic-declination'; 8 import 'leaflet-rotatedmarker'; // eslint-disable-line import/no-unassigned-import 9 import iconPointer from './pointer.svg'; 10 import iconPointerStart from './pointer-start.svg'; 11 import iconPointerEnd from './pointer-end.svg'; 12 import {ElevationProfile, calcSamplingInterval} from '~/lib/leaflet.control.elevation-profile'; 13 14 function radians(x) { 15 return x / 180 * Math.PI; 16 } 17 18 function degrees(x) { 19 return x / Math.PI * 180; 20 } 21 22 function calcAzimuth(latlng1, latlng2) { 23 const lat1 = radians(latlng1.lat); 24 const lat2 = radians(latlng2.lat); 25 const lng1 = radians(latlng1.lng); 26 const lng2 = radians(latlng2.lng); 27 28 const y = Math.sin(lng2 - lng1) * Math.cos(lat2); 29 const x = Math.cos(lat1) * Math.sin(lat2) - 30 Math.sin(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1); 31 let brng = Math.atan2(y, x); 32 brng = degrees(brng); 33 return brng; 34 } 35 36 function calcAngle(latlng1, latlng2) { 37 const p1 = L.Projection.SphericalMercator.project(latlng1); 38 const p2 = L.Projection.SphericalMercator.project(latlng2); 39 const delta = p2.subtract(p1); 40 const angle = Math.atan2(delta.x, delta.y); 41 return degrees(angle); 42 } 43 44 function roundAzimuth(a) { 45 return (Math.round(a) + 360) % 360; 46 } 47 48 L.Control.Azimuth = L.Control.extend({ 49 options: { 50 position: 'bottomleft' 51 }, 52 53 includes: L.Mixin.Events, 54 55 initialize: function(options) { 56 L.Control.prototype.initialize.call(this, options); 57 this.trueAzimuth = ko.observable(null); 58 this.magneticAzimuth = ko.observable(null); 59 this.distance = ko.observable(null); 60 this.points = { 61 start: null, 62 end: null 63 }; 64 const iconSingle = L.icon({iconUrl: iconPointer, iconSize: [30, 30]}); 65 const iconStart = L.icon({iconUrl: iconPointerStart, iconSize: [30, 30]}); 66 const iconEnd = L.icon({iconUrl: iconPointerEnd, iconSize: [30, 45]}); 67 this.azimuthLine = L.polyline([], {interactive: false, weight: 1.5}); 68 this.markers = { 69 single: L.marker([0, 0], {icon: iconSingle, draggable: true, which: 'start'}) 70 .on('drag', this.onMarkerDrag, this) 71 .on('click', L.DomEvent.stopPropagation), 72 start: L.marker([0, 0], { 73 icon: iconStart, 74 draggable: true, 75 which: 'start', 76 rotationOrigin: 'center center', 77 projectedShift: () => this.azimuthLine.shiftProjectedFitMapView() 78 }) 79 .on('drag', this.onMarkerDrag, this) 80 .on('click', L.DomEvent.stopPropagation) 81 .on('dragend', this.onMarkerDragEnd, this), 82 end: L.marker([0, 0], { 83 icon: iconEnd, 84 draggable: true, 85 which: 'end', 86 rotationOrigin: 'center center', 87 projectedShift: () => this.azimuthLine.shiftProjectedFitMapView() 88 }) 89 .on('drag', this.onMarkerDrag, this) 90 .on('click', L.DomEvent.stopPropagation) 91 .on('dragend', this.onMarkerDragEnd, this) 92 }; 93 }, 94 95 onAdd: function(map) { 96 this._map = map; 97 const {container, link, barContainer} = makeButtonWithBar( 98 'leaflet-control-azimuth', 'Measure bearing, display line of sight', 'icon-azimuth'); 99 this._container = container; 100 L.DomEvent.on(link, 'click', this.onClick, this); 101 102 barContainer.innerHTML = layout; 103 ko.applyBindings(this, barContainer); 104 return container; 105 }, 106 107 onClick: function() { 108 if (this.isEnabled()) { 109 this.disableControl(); 110 } else { 111 this.enableControl(); 112 } 113 }, 114 115 onMarkerDrag: function(e) { 116 const marker = e.target; 117 this.setPoints({[marker.options.which]: marker.getLatLng()}); 118 }, 119 120 onMarkerDragEnd: function() { 121 if (this.elevationControl) { 122 this.showProfile(); 123 } 124 }, 125 126 enableControl: function() { 127 L.DomUtil.addClass(this._container, 'active'); 128 L.DomUtil.addClass(this._container, 'highlight'); 129 L.DomUtil.addClass(this._map._container, 'azimuth-control-active'); 130 this._map.on('click', this.onMapClick, this); 131 this.fire('enabled'); 132 this._map.clickLocked = true; 133 this._enabled = true; 134 }, 135 136 disableControl: function() { 137 L.DomUtil.removeClass(this._container, 'active'); 138 L.DomUtil.removeClass(this._container, 'highlight'); 139 this.hideProfile(); 140 this.setPoints({start: null, end: null}); 141 L.DomUtil.removeClass(this._map._container, 'azimuth-control-active'); 142 this._map.off('click', this.onMapClick, this); 143 this._map.clickLocked = false; 144 this._enabled = false; 145 }, 146 147 isEnabled: function() { 148 return Boolean(this._enabled); 149 }, 150 151 setPoints: function(points) { 152 Object.assign(this.points, points); 153 points = this.points; 154 if (points.start && !points.end) { 155 this.markers.single 156 .setLatLng(points.start) 157 .addTo(this._map); 158 } else { 159 this.markers.single.removeFrom(this._map); 160 } 161 if (points.start && points.end) { 162 const angle = calcAngle(points.start, points.end); 163 this.markers.start 164 .setLatLng(points.start) 165 .addTo(this._map) 166 .setRotationAngle(angle); 167 this.markers.end 168 .setLatLng(points.end) 169 .addTo(this._map) 170 .setRotationAngle(angle); 171 this.azimuthLine 172 .setLatLngs([[points.start, points.end]]) 173 .addTo(this._map); 174 } else { 175 this.markers.start.removeFrom(this._map); 176 this.markers.end.removeFrom(this._map); 177 this.azimuthLine.removeFrom(this._map); 178 } 179 this.updateValuesDisplay(); 180 }, 181 182 updateValuesDisplay: function() { 183 if (this.points.start && this.points.end) { 184 const points = this.points; 185 const azimuth = calcAzimuth(points.start, points.end); 186 this.trueAzimuth(roundAzimuth(azimuth)); 187 const declination = getDeclination(points.start.lat, points.start.lng); 188 if (declination === null) { 189 this.magneticAzimuth(null); 190 } else { 191 this.magneticAzimuth(roundAzimuth(azimuth - declination)); 192 } 193 this.distance(points.start.distanceTo(points.end)); 194 } else { 195 this.distance(null); 196 this.trueAzimuth(null); 197 this.magneticAzimuth(null); 198 } 199 }, 200 201 onMapClick: function(e) { 202 if (!this.points.start && !this.points.end) { 203 this.setPoints({start: e.latlng}); 204 } else if (this.points.start && !this.points.end) { 205 this.setPoints({end: e.latlng}); 206 } else if (this.points.start && this.points.end) { 207 this.hideProfile(); 208 this.setPoints({start: e.latlng, end: null}); 209 } 210 }, 211 212 showProfile: function() { 213 if (!this.points.end) { 214 return; 215 } 216 if (this.elevationControl) { 217 this.elevationControl.removeFrom(this._map); 218 } 219 220 const dist = this.points.start.distanceTo(this.points.end); 221 this.elevationControl = new ElevationProfile(this._map, [this.points.start, this.points.end], { 222 samplingInterval: calcSamplingInterval(dist), 223 sightLine: true 224 }); 225 this.elevationControl.on('remove', () => { 226 this.elevationControl = null; 227 }); 228 this.fire('elevation-shown'); 229 }, 230 231 hideProfile: function() { 232 if (this.elevationControl) { 233 this.elevationControl.removeFrom(this._map); 234 } 235 this.elevationControl = null; 236 }, 237 238 onProfileButtonClick: function() { 239 this.showProfile(); 240 }, 241 242 onReverseButtonClick: function() { 243 if (this.points.end && this.points.start) { 244 this.setPoints({start: this.points.end, end: this.points.start}); 245 if (this.elevationControl) { 246 this.showProfile(); 247 } 248 } 249 } 250 251 } 252 ); 253