index.js (9373B)
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, magneticModelInfo} 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.magneticModelInfo = magneticModelInfo; 61 this.points = { 62 start: null, 63 end: null 64 }; 65 const iconSingle = L.icon({iconUrl: iconPointer, iconSize: [30, 30]}); 66 const iconStart = L.icon({iconUrl: iconPointerStart, iconSize: [30, 30]}); 67 const iconEnd = L.icon({iconUrl: iconPointerEnd, iconSize: [30, 45]}); 68 this.azimuthLine = L.polyline([], {interactive: false, weight: 1.5}); 69 this.markers = { 70 single: L.marker([0, 0], {icon: iconSingle, draggable: true, which: 'start'}) 71 .on('drag', this.onMarkerDrag, this) 72 .on('click', L.DomEvent.stopPropagation), 73 start: L.marker([0, 0], { 74 icon: iconStart, 75 draggable: true, 76 which: 'start', 77 rotationOrigin: 'center center', 78 projectedShift: () => this.azimuthLine.shiftProjectedFitMapView() 79 }) 80 .on('drag', this.onMarkerDrag, this) 81 .on('click', L.DomEvent.stopPropagation) 82 .on('dragend', this.onMarkerDragEnd, this), 83 end: L.marker([0, 0], { 84 icon: iconEnd, 85 draggable: true, 86 which: 'end', 87 rotationOrigin: 'center center', 88 projectedShift: () => this.azimuthLine.shiftProjectedFitMapView() 89 }) 90 .on('drag', this.onMarkerDrag, this) 91 .on('click', L.DomEvent.stopPropagation) 92 .on('dragend', this.onMarkerDragEnd, this) 93 }; 94 }, 95 96 onAdd: function(map) { 97 this._map = map; 98 const {container, link, barContainer} = makeButtonWithBar( 99 'leaflet-control-azimuth', 'Measure bearing, display line of sight', 'icon-azimuth'); 100 this._container = container; 101 L.DomEvent.on(link, 'click', this.onClick, this); 102 103 barContainer.innerHTML = layout; 104 ko.applyBindings(this, barContainer); 105 return container; 106 }, 107 108 onClick: function() { 109 if (this.isEnabled()) { 110 this.disableControl(); 111 } else { 112 this.enableControl(); 113 } 114 }, 115 116 onMarkerDrag: function(e) { 117 const marker = e.target; 118 this.setPoints({[marker.options.which]: marker.getLatLng()}); 119 }, 120 121 onMarkerDragEnd: function() { 122 if (this.elevationControl) { 123 this.showProfile(); 124 } 125 }, 126 127 enableControl: function() { 128 L.DomUtil.addClass(this._container, 'active'); 129 L.DomUtil.addClass(this._container, 'highlight'); 130 L.DomUtil.addClass(this._map._container, 'azimuth-control-active'); 131 this._map.on('click', this.onMapClick, this); 132 this.fire('enabled'); 133 this._map.clickLocked = true; 134 this._enabled = true; 135 }, 136 137 disableControl: function() { 138 L.DomUtil.removeClass(this._container, 'active'); 139 L.DomUtil.removeClass(this._container, 'highlight'); 140 this.hideProfile(); 141 this.setPoints({start: null, end: null}); 142 L.DomUtil.removeClass(this._map._container, 'azimuth-control-active'); 143 this._map.off('click', this.onMapClick, this); 144 this._map.clickLocked = false; 145 this._enabled = false; 146 }, 147 148 isEnabled: function() { 149 return Boolean(this._enabled); 150 }, 151 152 setPoints: function(points) { 153 Object.assign(this.points, points); 154 points = this.points; 155 if (points.start && !points.end) { 156 this.markers.single 157 .setLatLng(points.start) 158 .addTo(this._map); 159 } else { 160 this.markers.single.removeFrom(this._map); 161 } 162 if (points.start && points.end) { 163 const angle = calcAngle(points.start, points.end); 164 this.markers.start 165 .setLatLng(points.start) 166 .addTo(this._map) 167 .setRotationAngle(angle); 168 this.markers.end 169 .setLatLng(points.end) 170 .addTo(this._map) 171 .setRotationAngle(angle); 172 this.azimuthLine 173 .setLatLngs([[points.start, points.end]]) 174 .addTo(this._map); 175 } else { 176 this.markers.start.removeFrom(this._map); 177 this.markers.end.removeFrom(this._map); 178 this.azimuthLine.removeFrom(this._map); 179 } 180 this.updateValuesDisplay(); 181 }, 182 183 updateValuesDisplay: function() { 184 if (this.points.start && this.points.end) { 185 const points = this.points; 186 const azimuth = calcAzimuth(points.start, points.end); 187 this.trueAzimuth(roundAzimuth(azimuth)); 188 const declination = getDeclination(points.start.lat, points.start.lng); 189 if (declination === null) { 190 this.magneticAzimuth(null); 191 } else { 192 this.magneticAzimuth(roundAzimuth(azimuth - declination)); 193 } 194 this.distance(points.start.distanceTo(points.end)); 195 } else { 196 this.distance(null); 197 this.trueAzimuth(null); 198 this.magneticAzimuth(null); 199 } 200 }, 201 202 onMapClick: function(e) { 203 if (!this.points.start && !this.points.end) { 204 this.setPoints({start: e.latlng}); 205 } else if (this.points.start && !this.points.end) { 206 this.setPoints({end: e.latlng}); 207 } else if (this.points.start && this.points.end) { 208 this.hideProfile(); 209 this.setPoints({start: e.latlng, end: null}); 210 } 211 }, 212 213 showProfile: function() { 214 if (!this.points.end) { 215 return; 216 } 217 if (this.elevationControl) { 218 this.elevationControl.removeFrom(this._map); 219 } 220 221 const dist = this.points.start.distanceTo(this.points.end); 222 this.elevationControl = new ElevationProfile(this._map, [this.points.start, this.points.end], { 223 samplingInterval: calcSamplingInterval(dist), 224 sightLine: true 225 }); 226 this.elevationControl.on('remove', () => { 227 this.elevationControl = null; 228 }); 229 this.fire('elevation-shown'); 230 }, 231 232 hideProfile: function() { 233 if (this.elevationControl) { 234 this.elevationControl.removeFrom(this._map); 235 } 236 this.elevationControl = null; 237 }, 238 239 onProfileButtonClick: function() { 240 this.showProfile(); 241 }, 242 243 onReverseButtonClick: function() { 244 if (this.points.end && this.points.start) { 245 this.setPoints({start: this.points.end, end: this.points.start}); 246 if (this.elevationControl) { 247 this.showProfile(); 248 } 249 } 250 } 251 252 } 253 ); 254