├── .gitignore ├── LICENSE.txt ├── Leaflet.PolylineMeasure.css ├── Leaflet.PolylineMeasure.js ├── README.md ├── _config.yml ├── demo1.html ├── demo2.html ├── demo3.html ├── demo4.html ├── demo5.html ├── package.json └── screenshot.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2012-2013, Jürgen Treml 4 | Copyright (c) 2017, PPete 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /Leaflet.PolylineMeasure.css: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | ** ** 3 | ** Leaflet Plugin "Leaflet.PolylineMeasure" ** 4 | ** File "Leaflet.PolylineMeasure.css" ** 5 | ** Date: 2023-05-26 ** 6 | ** ** 7 | *********************************************************/ 8 | 9 | 10 | a.polyline-measure-controlOnBgColor, a.polyline-measure-controlOnBgColor:hover { 11 | background-color: #8f8; 12 | } 13 | 14 | .polyline-measure-unicode-icon { 15 | font-size: 19px; 16 | font-weight: bold; 17 | } 18 | 19 | a.polyline-measure-clearControl:active { 20 | background-color: #f88; 21 | } 22 | 23 | .polyline-measure-unicode-icon, .polyline-measure-clearControl, #unitControlId 24 | { 25 | cursor: pointer; 26 | } 27 | 28 | .polyline-measure-tooltip { 29 | font: 10px Arial, Helvetica, sans-serif; 30 | line-height: 10px; 31 | background-color: rgba(255, 255, 170, 0.7); 32 | border-radius: 3px; 33 | box-shadow: 1px 1px 4px #888; 34 | margin: 0; 35 | padding: 2px; 36 | width: auto !important; 37 | height: auto !important; 38 | white-space: nowrap; 39 | text-align: right; 40 | } 41 | 42 | .polyline-measure-tooltip-end { 43 | background-color: rgba(255, 255, 40, 0.7); 44 | } 45 | 46 | .polyline-measure-tooltip-total { 47 | color: #006; 48 | font-weight: bold; 49 | } 50 | 51 | .polyline-measure-tooltip-difference { 52 | color: #060; 53 | font-style: italic; 54 | } 55 | 56 | .polyline-measure-popupTooltip { 57 | font: 11px Arial, Helvetica, sans-serif; 58 | line-height: 11px; 59 | } 60 | -------------------------------------------------------------------------------- /Leaflet.PolylineMeasure.js: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | ** ** 3 | ** Leaflet Plugin "Leaflet.PolylineMeasure" ** 4 | ** File "Leaflet.PolylineMeasure.js" ** 5 | ** Date: 2023-11-07 ** 6 | ** ** 7 | *********************************************************/ 8 | 9 | 10 | (function (factory) { 11 | if (typeof define === 'function' && define.amd) { 12 | // AMD 13 | define(['leaflet'], factory); 14 | } else if (typeof module !== 'undefined') { 15 | // Node/CommonJS 16 | module.exports = factory(require('leaflet')); 17 | } else { 18 | // Browser globals 19 | if (typeof window.L === 'undefined') { 20 | throw new Error('Leaflet must be loaded first'); 21 | } 22 | factory(window.L); 23 | } 24 | }(function (L) { 25 | var _measureControlId = 'polyline-measure-control'; 26 | var _unicodeClass = 'polyline-measure-unicode-icon'; 27 | var isMacOS = navigator.platform === 'MacIntel'; 28 | 29 | /** 30 | * Polyline Measure class 31 | * @extends L.Control 32 | */ 33 | L.Control.PolylineMeasure = L.Control.extend({ 34 | /** 35 | * Default options for the tool 36 | * @type {Object} 37 | */ 38 | options: { 39 | /** 40 | * Position to show the control. Possible values are: 'topright', 'topleft', 'bottomright', 'bottomleft' 41 | * @type {String} 42 | * @default 43 | */ 44 | position: 'topleft', 45 | /** 46 | * Default unit the distances are displayed in. Possible values are: 'kilometres', 'landmiles', 'nauticalmiles' 47 | * @type {String} 48 | * @default 49 | */ 50 | unit: 'kilometres', 51 | /** 52 | * Use subunits (metres/feet) in tooltips in case of distances less then 1 kilometre/landmile 53 | * @type {Boolean} 54 | * @default 55 | */ 56 | useSubunits: true, 57 | /** 58 | * Clear all measurements when Measure Control is switched off 59 | * @type {Boolean} 60 | * @default 61 | */ 62 | clearMeasurementsOnStop: true, 63 | /** 64 | * Whether bearings are displayed within the tooltips 65 | * @type {Boolean} 66 | * @default 67 | */ 68 | showBearings: false, 69 | /** 70 | * Text for the bearing In 71 | * @type {String} 72 | * @default 73 | */ 74 | bearingTextIn: 'In', 75 | /** 76 | * Text for the bearing Out 77 | * @type {String} 78 | * @default 79 | */ 80 | bearingTextOut: 'Out', 81 | /** 82 | * Text for last point's tooltip 83 | * @type {String} 84 | * @default 85 | */ 86 | tooltipTextFinish: 'Click to finish line
', 87 | tooltipTextDelete: 'Press SHIFT-key and click to delete point', 88 | tooltipTextMove: 'Click and drag to move point
', 89 | tooltipTextResume: '
Press ' + (isMacOS ? '⌘' : 'CTRL-key') + ' and click to resume line', 90 | tooltipTextAdd: 'Press ' + (isMacOS ? '⌘' : 'CTRL-key') + ' and click to add point', 91 | 92 | /** 93 | * Title for the control going to be switched on 94 | * @type {String} 95 | * @default 96 | */ 97 | measureControlTitleOn: "Turn on PolylineMeasure", 98 | /** 99 | * Title for the control going to be switched off 100 | * @type {String} 101 | * @default 102 | */ 103 | measureControlTitleOff: "Turn off PolylineMeasure", 104 | /** 105 | * Label of the Measure control (maybe a unicode symbol) 106 | * @type {String} 107 | * @default 108 | */ 109 | measureControlLabel: '↦', 110 | /** 111 | * Classes to apply to the Measure control 112 | * @type {Array} 113 | * @default 114 | */ 115 | measureControlClasses: [], 116 | /** 117 | * Show a control to clear all the measurements 118 | * @type {Boolean} 119 | * @default 120 | */ 121 | showClearControl: false, 122 | /** 123 | * Title text to show on the Clear measurements control button 124 | * @type {String} 125 | * @default 126 | */ 127 | clearControlTitle: 'Clear Measurements', 128 | /** 129 | * Label of the Clear control (maybe a unicode symbol) 130 | * @type {String} 131 | * @default 132 | */ 133 | clearControlLabel: '×', 134 | /** 135 | * Classes to apply to Clear control button 136 | * @type {Array} 137 | * @default 138 | */ 139 | clearControlClasses: [], 140 | /** 141 | * Show a control to change the units of measurements 142 | * @type {Boolean} 143 | * @default 144 | */ 145 | showUnitControl: false, 146 | /** 147 | * The measurement units that can be cycled through by using the Unit Control button 148 | * @type {Array} 149 | * @default 150 | */ 151 | unitControlUnits: ["kilometres" , "landmiles", "nauticalmiles"], 152 | /** 153 | * Title texts to show on the Unit Control button 154 | * @type {Object} 155 | * @default 156 | */ 157 | unitControlTitle: { 158 | text: 'Change Units', 159 | kilometres: 'kilometres', 160 | landmiles: 'land miles', 161 | nauticalmiles: 'nautical miles' 162 | }, 163 | /** 164 | * Unit symbols to show in the Unit Control button and measurement labels 165 | * @type {Object} 166 | * @default 167 | */ 168 | unitControlLabel: { 169 | metres: 'm', 170 | kilometres: 'km', 171 | feet: 'ft', 172 | landmiles: 'mi', 173 | nauticalmiles: 'nm' 174 | }, 175 | /** 176 | * Classes to apply to the Unit control 177 | * @type {Array} 178 | * @default 179 | */ 180 | unitControlClasses: [], 181 | 182 | /** 183 | * Styling settings for the temporary dashed rubberline 184 | * @type {Object} 185 | */ 186 | tempLine: { 187 | /** 188 | * Dashed line color 189 | * @type {String} 190 | * @default 191 | */ 192 | color: '#00f', 193 | /** 194 | * Dashed line weight 195 | * @type {Number} 196 | * @default 197 | */ 198 | weight: 2 199 | }, 200 | /** 201 | * Styling for the fixed polyline 202 | * @type {Object} 203 | */ 204 | fixedLine: { 205 | /** 206 | * Solid line color 207 | * @type {String} 208 | * @default 209 | */ 210 | color: '#006', 211 | /** 212 | * Solid line weight 213 | * @type {Number} 214 | * @default 215 | */ 216 | weight: 2 217 | }, 218 | /** 219 | * Styling of the midway arrow 220 | * @type {Object} 221 | */ 222 | arrow: { 223 | /** 224 | * Color of the arrow 225 | * @type {String} 226 | * @default 227 | */ 228 | color: '#000' 229 | }, 230 | /** 231 | * Style settings for circle marker indicating the starting point of the polyline 232 | * @type {Object} 233 | */ 234 | startCircle: { 235 | /** 236 | * Color of the border of the circle 237 | * @type {String} 238 | * @default 239 | */ 240 | color: '#000', 241 | /** 242 | * Weight of the circle 243 | * @type {Number} 244 | * @default 245 | */ 246 | weight: 1, 247 | /** 248 | * Fill color of the circle 249 | * @type {String} 250 | * @default 251 | */ 252 | fillColor: '#0f0', 253 | /** 254 | * Fill opacity of the circle 255 | * @type {Number} 256 | * @default 257 | */ 258 | fillOpacity: 1, 259 | /** 260 | * Radius of the circle 261 | * @type {Number} 262 | * @default 263 | */ 264 | radius: 3 265 | }, 266 | /** 267 | * Style settings for all circle markers between startCircle and endCircle 268 | * @type {Object} 269 | */ 270 | intermedCircle: { 271 | /** 272 | * Color of the border of the circle 273 | * @type {String} 274 | * @default 275 | */ 276 | color: '#000', 277 | /** 278 | * Weight of the circle 279 | * @type {Number} 280 | * @default 281 | */ 282 | weight: 1, 283 | /** 284 | * Fill color of the circle 285 | * @type {String} 286 | * @default 287 | */ 288 | fillColor: '#ff0', 289 | /** 290 | * Fill opacity of the circle 291 | * @type {Number} 292 | * @default 293 | */ 294 | fillOpacity: 1, 295 | /** 296 | * Radius of the circle 297 | * @type {Number} 298 | * @default 299 | */ 300 | radius: 3 301 | }, 302 | /** 303 | * Style settings for circle marker indicating the latest point of the polyline during drawing a line 304 | * @type {Object} 305 | */ 306 | currentCircle: { 307 | /** 308 | * Color of the border of the circle 309 | * @type {String} 310 | * @default 311 | */ 312 | color: '#000', 313 | /** 314 | * Weight of the circle 315 | * @type {Number} 316 | * @default 317 | */ 318 | weight: 1, 319 | /** 320 | * Fill color of the circle 321 | * @type {String} 322 | * @default 323 | */ 324 | fillColor: '#f0f', 325 | /** 326 | * Fill opacity of the circle 327 | * @type {Number} 328 | * @default 329 | */ 330 | fillOpacity: 1, 331 | /** 332 | * Radius of the circle 333 | * @type {Number} 334 | * @default 335 | */ 336 | radius: 6 337 | }, 338 | /** 339 | * Style settings for circle marker indicating the end point of the polyline 340 | * @type {Object} 341 | */ 342 | endCircle: { 343 | /** 344 | * Color of the border of the circle 345 | * @type {String} 346 | * @default 347 | */ 348 | color: '#000', 349 | /** 350 | * Weight of the circle 351 | * @type {Number} 352 | * @default 353 | */ 354 | weight: 1, 355 | /** 356 | * Fill color of the circle 357 | * @type {String} 358 | * @default 359 | */ 360 | fillColor: '#f00', 361 | /** 362 | * Fill opacity of the circle 363 | * @type {Number} 364 | * @default 365 | */ 366 | fillOpacity: 1, 367 | /** 368 | * Radius of the circle 369 | * @type {Number} 370 | * @default 371 | */ 372 | radius: 3 373 | } 374 | }, 375 | 376 | _arcpoints: 99, // 99 points = 98 line segments. lower value to make arc less accurate or increase value to make it more accurate. 377 | _circleNr: -1, 378 | _lineNr: -1, 379 | 380 | /** 381 | * Create a control button 382 | * @param {String} label Label to add 383 | * @param {String} title Title to show on hover 384 | * @param {Array} classesToAdd Collection of classes to add 385 | * @param {Element} container Parent element 386 | * @param {Function} fn Callback function to run 387 | * @param {Object} context Context 388 | * @returns {Element} Created element 389 | * @private 390 | */ 391 | _createControl: function (label, title, classesToAdd, container, fn, context) { 392 | var anchor = document.createElement('a'); 393 | anchor.innerHTML = label; 394 | anchor.setAttribute('title', title); 395 | classesToAdd.forEach(function(c) { 396 | anchor.classList.add(c); 397 | }); 398 | L.DomEvent.on (anchor, 'click', fn, context); 399 | container.appendChild(anchor); 400 | return anchor; 401 | }, 402 | 403 | /** 404 | * Method to fire on add to map 405 | * @param {Object} map Map object 406 | * @returns {Element} Containing element 407 | */ 408 | onAdd: function(map) { 409 | var self = this 410 | // needed to avoid creating points by mouseclick during dragging the map 411 | map.on('movestart ', function() { 412 | self._mapdragging = true 413 | }) 414 | this._container = document.createElement('div'); 415 | this._container.classList.add('leaflet-bar'); 416 | L.DomEvent.disableClickPropagation(this._container); // otherwise drawing process would instantly start at controls' container or double click would zoom-in map 417 | var title = this.options.measureControlTitleOn; 418 | var label = this.options.measureControlLabel; 419 | var classes = this.options.measureControlClasses; 420 | if (label.indexOf('&') != -1) { 421 | classes.push(_unicodeClass); 422 | } 423 | 424 | // initialize state 425 | this._arrPolylines = []; 426 | this._measureControl = this._createControl (label, title, classes, this._container, this._toggleMeasure, this); 427 | this._defaultControlBgColor = this._measureControl.style.backgroundColor; 428 | this._measureControl.setAttribute('id', _measureControlId); 429 | if (this.options.showClearControl) { 430 | var title = this.options.clearControlTitle; 431 | var label = this.options.clearControlLabel; 432 | var classes = this.options.clearControlClasses; 433 | if (label.indexOf('&') != -1) { 434 | classes.push(_unicodeClass); 435 | } 436 | this._clearMeasureControl = this._createControl (label, title, classes, this._container, this._clearAllMeasurements, this); 437 | this._clearMeasureControl.classList.add('polyline-measure-clearControl') 438 | } 439 | 440 | // There is no point in using the unitControl if there are no units to choose from. 441 | if (this.options.showUnitControl && this.options.unitControlUnits.length > 1) { 442 | var label = this.options.unitControlLabel[this.options.unit]; 443 | var title = this.options.unitControlTitle.text + " [" + this.options.unitControlTitle[this.options.unit] + "]"; 444 | 445 | var classes = this.options.unitControlClasses; 446 | this._unitControl = this._createControl (label, title, classes, this._container, this._changeUnit, this); 447 | this._unitControl.setAttribute ('id', 'unitControlId'); 448 | } 449 | return this._container; 450 | }, 451 | 452 | /** 453 | * Method to fire on remove from map 454 | */ 455 | onRemove: function () { 456 | if (this._measuring) { 457 | this._toggleMeasure(); 458 | } 459 | }, 460 | 461 | // turn off all Leaflet-own events of markers (popups, tooltips). Needed to allow creating points on top of markers 462 | _saveNonpolylineEvents: function () { 463 | this._nonpolylineTargets = this._map._targets; 464 | if (typeof this._polylineTargets !== 'undefined') { 465 | this._map._targets = this._polylineTargets; 466 | } else { 467 | this._map._targets ={}; 468 | } 469 | }, 470 | 471 | // on disabling the measure add-on, save Polyline-measure events and enable the former Leaflet-own events again 472 | _savePolylineEvents: function () { 473 | this._polylineTargets = this._map._targets; 474 | this._map._targets = this._nonpolylineTargets; 475 | }, 476 | 477 | /** 478 | * Toggle the measure functionality on or off 479 | * @private 480 | */ 481 | _toggleMeasure: function () { 482 | this._measuring = !this._measuring; 483 | if (this._measuring) { // if measuring is going to be switched on 484 | this._mapdragging = false; 485 | this._saveNonpolylineEvents (); 486 | this._measureControl.classList.add ('polyline-measure-controlOnBgColor'); 487 | this._measureControl.style.backgroundColor = this.options.backgroundColor; 488 | this._measureControl.title = this.options.measureControlTitleOff; 489 | this._oldCursor = this._map._container.style.cursor; // save former cursor type 490 | this._map._container.style.cursor = 'crosshair'; 491 | this._doubleClickZoom = this._map.doubleClickZoom.enabled(); // save former status of doubleClickZoom 492 | this._map.doubleClickZoom.disable(); 493 | // create LayerGroup "layerPaint" (only) the first time Measure Control is switched on 494 | if (!this._layerPaint) { 495 | this._layerPaint = L.layerGroup().addTo(this._map); 496 | } 497 | this._map.on ('mousemove', this._mouseMove, this); // enable listing to 'mousemove', 'click', 'keydown' events 498 | this._map.on ('click', this._mouseClick, this); 499 | L.DomEvent.on (document, 'keydown', this._onKeyDown, this); 500 | this._resetPathVariables(); 501 | } else { // if measuring is going to be switched off 502 | this._savePolylineEvents (); 503 | this._measureControl.classList.remove ('polyline-measure-controlOnBgColor'); 504 | this._measureControl.style.backgroundColor = this._defaultControlBgColor; 505 | this._measureControl.title = this.options.measureControlTitleOn; 506 | this._map._container.style.cursor = this._oldCursor; 507 | this._map.off ('mousemove', this._mouseMove, this); 508 | this._map.off ('click', this._mouseClick, this); 509 | this._map.off ('mousemove', this._resumeFirstpointMousemove, this); 510 | this._map.off ('click', this._resumeFirstpointClick, this); 511 | this._map.off ('mousemove', this._dragCircleMousemove, this); 512 | this._map.off ('mouseup', this._dragCircleMouseup, this); 513 | L.DomEvent.off (document, 'keydown', this._onKeyDown, this); 514 | if(this._doubleClickZoom) { 515 | this._map.doubleClickZoom.enable(); 516 | } 517 | if(this.options.clearMeasurementsOnStop && this._layerPaint) { 518 | this._clearAllMeasurements(); 519 | } 520 | // to remove temp. Line if line at the moment is being drawn and not finished while clicking the control 521 | if (this._cntCircle !== 0) { 522 | this._finishPolylinePath(); 523 | } 524 | } 525 | // allow easy to connect the measure control to the app, f.e. to disable the selection on the map when the measurement is turned on 526 | this._map.fire('polylinemeasure:toggle', { status: this._measuring }); 527 | }, 528 | 529 | /** 530 | * Clear all measurements from the map 531 | */ 532 | _clearAllMeasurements: function() { 533 | if ((this._cntCircle !== undefined) && (this._cntCircle !== 0)) { 534 | this._finishPolylinePath(); 535 | } 536 | if (this._layerPaint) { 537 | this._layerPaint.clearLayers(); 538 | } 539 | this._arrPolylines = []; 540 | this._map.fire('polylinemeasure:clear'); 541 | }, 542 | 543 | _changeUnit: function() { 544 | // Retrieve the index of the next available unit of measurement. 545 | let indexCurrUnit = this.options.unitControlUnits.indexOf(this.options.unit); 546 | let indexNextUnit = (indexCurrUnit+1)%this.options.unitControlUnits.length; 547 | 548 | // Update the unit of measurement. 549 | this.options.unit = this.options.unitControlUnits[indexNextUnit]; 550 | this._unitControl.innerHTML = this.options.unitControlLabel[this.options.unit]; 551 | this._unitControl.title = this.options.unitControlTitle.text +" [" + this.options.unitControlTitle[this.options.unit] + "]"; 552 | 553 | if (this._currentLine) { 554 | this._computeDistance(this._currentLine); 555 | } 556 | this._arrPolylines.map (this._computeDistance.bind(this)); 557 | }, 558 | 559 | _computeDistance: function(line) { 560 | var totalDistance = 0; 561 | line.circleCoords.map (function(point, point_index) { 562 | if (point_index >= 1) { 563 | var distance = line.circleCoords [point_index - 1].distanceTo (line.circleCoords [point_index]); 564 | totalDistance += distance; 565 | this._updateTooltip (line.tooltips [point_index], line.tooltips [point_index - 1], totalDistance, distance, line.circleCoords [point_index - 1], line.circleCoords [point_index]); 566 | } 567 | }.bind(this)); 568 | }, 569 | 570 | /** 571 | * Event to fire when a keyboard key is depressed. 572 | * Currently only watching for ESC key (= keyCode 27). 1st press finishes line, 2nd press turns Measuring off. 573 | * @param {Object} e Event 574 | * @private 575 | */ 576 | _onKeyDown: function (e) { 577 | if (e.keyCode === 27) { 578 | // if resuming a line at its first point is active 579 | if (this._resumeFirstpointFlag === true) { 580 | this._resumeFirstpointFlag = false; 581 | var lineNr = this._lineNr; 582 | this._map.off ('mousemove', this._resumeFirstpointMousemove, this); 583 | this._map.off ('click', this._resumeFirstpointClick, this); 584 | this._layerPaint.removeLayer (this._rubberlinePath2); 585 | this._layerPaint.removeLayer (this._tooltipNew); 586 | this._arrPolylines[lineNr].circleMarkers [0].setStyle (this.options.startCircle); 587 | var text = ''; 588 | var totalDistance = 0; 589 | if (this.options.showBearings === true) { 590 | text = this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; 591 | } 592 | text = text + '
+' + '0
'; 593 | text = text + '
' + '0
'; 594 | this._arrPolylines[lineNr].tooltips [0]._icon.innerHTML = text; 595 | this._arrPolylines[lineNr].tooltips.map (function (item, index) { 596 | if (index >= 1) { 597 | var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); 598 | var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; 599 | var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; 600 | totalDistance += distance; 601 | var prevTooltip = this._arrPolylines[lineNr].tooltips[index-1] 602 | this._updateTooltip (item, prevTooltip, totalDistance, distance, lastCircleCoords, mouseCoords); 603 | } 604 | }.bind (this)); 605 | this._map.on ('mousemove', this._mouseMove, this); 606 | return 607 | } 608 | if (!this._currentLine) { // if NOT drawing a line, ESC will directly switch of measuring 609 | this._toggleMeasure(); 610 | } else { // if drawing a line, ESC will finish the current line 611 | this._finishPolylinePath(e); 612 | } 613 | } 614 | }, 615 | 616 | /** 617 | * Get the distance in the format specified in the options 618 | * @param {Number} distance Distance to convert 619 | * @returns {{value: *, unit: *}} 620 | * @private 621 | */ 622 | _getDistance: function (distance) { 623 | var dist = distance; 624 | var unit; 625 | if (this.options.unit === 'nauticalmiles') { 626 | unit = this.options.unitControlLabel.nauticalmiles; 627 | if (dist >= 185200) { 628 | dist = (dist/1852).toFixed(0); 629 | } else if (dist >= 18520) { 630 | dist = (dist/1852).toFixed(1); 631 | } else if (dist >= 1852) { 632 | dist = (dist/1852).toFixed(2); 633 | } else { 634 | dist = (dist/1852).toFixed(3); // there's no subunit of Nautical Miles for horizontal length measurements. "Cable length" (1/10th of 1 nm) is rarely used 635 | } 636 | } else if (this.options.unit === 'landmiles') { 637 | unit = this.options.unitControlLabel.landmiles; 638 | if (dist >= 160934.4) { 639 | dist = (dist/1609.344).toFixed(0); 640 | } else if (dist >= 16093.44) { 641 | dist = (dist/1609.344).toFixed(1); 642 | } else if (dist >= 1609.344) { 643 | dist = (dist/1609.344).toFixed(2); 644 | } else { 645 | if (!this.options.useSubunits) { 646 | dist = (dist/1609.344).toFixed(4); 647 | } else { 648 | dist = (dist/0.3048).toFixed(0); 649 | unit = this.options.unitControlLabel.feet; 650 | } 651 | } 652 | } 653 | else { 654 | unit = this.options.unitControlLabel.kilometres; 655 | if (dist >= 100000) { 656 | dist = (dist/1000).toFixed(0); 657 | } else if (dist >= 10000) { 658 | dist = (dist/1000).toFixed(1); 659 | } else if (dist >= 1000) { 660 | dist = (dist/1000).toFixed(2); 661 | } else { 662 | if (!this.options.useSubunits) { 663 | dist = (dist/1000).toFixed(4); 664 | } else { 665 | dist = (dist).toFixed(1); 666 | unit = this.options.unitControlLabel.metres; 667 | } 668 | } 669 | } 670 | return {value:dist, unit:unit}; 671 | }, 672 | 673 | /** 674 | * Calculate Great-circle Arc (= shortest distance on a sphere like the Earth) between two coordinates 675 | * formulas: http://www.edwilliams.org/avform.htm 676 | * @private 677 | */ 678 | _polylineArc: function (_from, _to) { 679 | 680 | function _GCinterpolate (f) { 681 | var A = Math.sin((1 - f) * d) / Math.sin(d); 682 | var B = Math.sin(f * d) / Math.sin(d); 683 | var x = A * Math.cos(fromLat) * Math.cos(fromLng) + B * Math.cos(toLat) * Math.cos(toLng); 684 | var y = A * Math.cos(fromLat) * Math.sin(fromLng) + B * Math.cos(toLat) * Math.sin(toLng); 685 | var z = A * Math.sin(fromLat) + B * Math.sin(toLat); 686 | // atan2 better than atan-function cause results are from -pi to +pi 687 | // => results of latInterpol, lngInterpol always within range -180° ... +180° => conversion into values < -180° or > + 180° has to be done afterwards 688 | var latInterpol = 180 / Math.PI * Math.atan2(z, Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))); 689 | var lngInterpol = 180 / Math.PI * Math.atan2(y, x); 690 | // don't split polyline if it crosses dateline ( -180° or +180°). Without the polyline would look like: +179° ==> +180° ==> -180° ==> -179°... 691 | // algo: if difference lngInterpol-from.lng is > 180° there's been an unwanted split from +180 to -180 cause an arc can never span >180° 692 | var diff = lngInterpol-fromLng*180/Math.PI; 693 | function trunc(n) { return Math[n > 0 ? "floor" : "ceil"](n); } // alternatively we could use the new Math.trunc method, but Internet Explorer doesn't know it 694 | if (diff < 0) { 695 | lngInterpol = lngInterpol - trunc ((diff - 180)/ 360) * 360; 696 | } else { 697 | lngInterpol = lngInterpol - trunc ((diff +180)/ 360) * 360; 698 | } 699 | return [latInterpol, lngInterpol]; 700 | } 701 | 702 | function _GCarc (npoints) { 703 | var arrArcCoords = []; 704 | var delta = 1.0 / (npoints-1 ); 705 | // first point of Arc should NOT be returned 706 | for (var i = 0; i < npoints; i++) { 707 | var step = delta * i; 708 | var coordPair = _GCinterpolate (step); 709 | arrArcCoords.push (coordPair); 710 | } 711 | return arrArcCoords; 712 | } 713 | 714 | var fromLat = _from.lat; // work with with copies of object's elements _from.lat and _from.lng, otherwise they would get modiefied due to call-by-reference on Objects in Javascript 715 | var fromLng = _from.lng; 716 | var toLat = _to.lat; 717 | var toLng = _to.lng; 718 | fromLat=fromLat * Math.PI / 180; 719 | fromLng=fromLng * Math.PI / 180; 720 | toLat=toLat * Math.PI / 180; 721 | toLng=toLng * Math.PI / 180; 722 | var d = 2.0 * Math.asin(Math.sqrt(Math.pow (Math.sin((fromLat - toLat) / 2.0), 2) + Math.cos(fromLat) * Math.cos(toLat) * Math.pow(Math.sin((fromLng - toLng) / 2.0), 2))); 723 | var arrLatLngs; 724 | if (d === 0) { 725 | arrLatLngs = [[fromLat, fromLng]]; 726 | } else { 727 | arrLatLngs = _GCarc(this._arcpoints); 728 | } 729 | return arrLatLngs; 730 | }, 731 | 732 | /** 733 | * Update the tooltip distance 734 | * @param {Number} total Total distance 735 | * @param {Number} difference Difference in distance between 2 points 736 | * @private 737 | */ 738 | _updateTooltip: function (currentTooltip, prevTooltip, total, difference, lastCircleCoords, mouseCoords) { 739 | // Explanation of formula: http://www.movable-type.co.uk/scripts/latlong.html 740 | var calcAngle = function (p1, p2, direction) { 741 | var lat1 = p1.lat / 180 * Math.PI; 742 | var lat2 = p2.lat / 180 * Math.PI; 743 | var lng1 = p1.lng / 180 * Math.PI; 744 | var lng2 = p2.lng / 180 * Math.PI; 745 | var y = Math.sin(lng2-lng1) * Math.cos(lat2); 746 | var x = Math.cos(lat1)*Math.sin(lat2) - Math.sin(lat1)*Math.cos(lat2)*Math.cos(lng2-lng1); 747 | if (direction === "inbound") { 748 | var brng = (Math.atan2(y, x) * 180 / Math.PI + 180).toFixed(0); 749 | } else { 750 | var brng = (Math.atan2(y, x) * 180 / Math.PI + 360).toFixed(0); 751 | } 752 | return (brng % 360); 753 | } 754 | 755 | var angleIn = calcAngle (mouseCoords, lastCircleCoords, "inbound"); 756 | var angleOut = calcAngle (lastCircleCoords, mouseCoords, "outbound"); 757 | var totalRound = this._getDistance (total); 758 | var differenceRound = this._getDistance (difference); 759 | var textCurrent = ''; 760 | if (differenceRound.value > 0 ) { 761 | if (this.options.showBearings === true) { 762 | textCurrent = this.options.bearingTextIn + ': ' + angleIn + '°
'+this.options.bearingTextOut+':---°'; 763 | } 764 | textCurrent += '
+' + differenceRound.value + ' ' + differenceRound.unit + '
'; 765 | } 766 | textCurrent += '
' + totalRound.value + ' ' + totalRound.unit + '
'; 767 | currentTooltip._icon.innerHTML = textCurrent; 768 | if ((this.options.showBearings === true) && (prevTooltip)) { 769 | var textPrev = prevTooltip._icon.innerHTML; 770 | var regExp = new RegExp(this.options.bearingTextOut + '.*\°'); 771 | var textReplace = textPrev.replace(regExp, this.options.bearingTextOut + ': ' + angleOut + "°"); 772 | prevTooltip._icon.innerHTML = textReplace; 773 | } 774 | }, 775 | 776 | _drawArrow: function (arcLine) { 777 | // center of Great-circle distance, NOT of the arc on a Mercator map! reason: a) to complicated b) map not always Mercator c) good optical feature to see where real center of distance is not the "virtual" warped arc center due to Mercator projection 778 | // differ between even and odd pointed Arcs. If even the arrow is in the center of the middle line-segment, if odd it is on the middle point 779 | var midpoint = Math.trunc(arcLine.length/2); 780 | if (arcLine.length % 2 == 0) { 781 | var P1 = arcLine[midpoint-1]; 782 | var P2 = arcLine[midpoint]; 783 | var diffLng12 = P2[1] - P1[1]; 784 | var diffLat12 = P2[0] - P1[0]; 785 | var center = [P1[0] + diffLat12/2, P1[1] + diffLng12/2]; 786 | } else { 787 | var P1 = arcLine[midpoint-1]; 788 | var P2 = arcLine[midpoint+1]; 789 | var diffLng12 = P2[1] - P1[1]; 790 | var diffLat12 = P2[0] - P1[0]; 791 | var center = arcLine[midpoint]; 792 | } 793 | // angle just an aprroximation, which could be somewhat off if Line runs near high latitudes. Use of *geographical coords* for line segment P1 to P2 is best method. Use of *Pixel coords* for just one arc segement P1 to P2 could create for short lines unexact rotation angles, and the use Use of Pixel coords between endpoints [0] to [98] (in case of 99-point-arc) results in even more rotation difference for high latitudes as with geogrpaphical coords-method 794 | var cssAngle = -Math.atan2(diffLat12, diffLng12)*57.29578 // convert radiant to degree as needed for use as CSS value; cssAngle is opposite to mathematical angle. 795 | var iconArrow = L.divIcon ({ 796 | className: "", // to avoid getting a default class with paddings and borders assigned by Leaflet 797 | iconSize: [16, 16], 798 | iconAnchor: [8, 8], 799 | // html : "" <<=== alternative method by the use of an image instead of a Unicode symbol. 800 | html : "
" // best results if iconSize = font-size = line-height and iconAnchor font-size/2 .both values needed to position symbol in center of L.divIcon for all font-sizes. 801 | }); 802 | var newArrowMarker = L.marker (center, {icon: iconArrow, zIndexOffset:-50}).addTo(this._layerPaint); // zIndexOffset to draw arrows below tooltips 803 | if (!this._currentLine){ // just bind tooltip if not drawing line anymore, cause following the instruction of tooltip is just possible when not drawing a line 804 | newArrowMarker.bindTooltip (this.options.tooltipTextAdd, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 805 | } 806 | newArrowMarker.on ('click', this._clickedArrow, this); 807 | return newArrowMarker; 808 | }, 809 | 810 | /** 811 | * Event to fire on mouse move 812 | * @param {Object} e Event 813 | * @private 814 | */ 815 | _mouseMove: function (e) { 816 | var mouseCoords = e.latlng; 817 | this._map.on ('click', this._mouseClick, this); // necassary for _dragCircle. If switched on already within _dragCircle an unwanted click is fired at the end of the drag. 818 | if(!mouseCoords || !this._currentLine) { 819 | return; 820 | } 821 | var lastCircleCoords = this._currentLine.circleCoords.last(); 822 | this._rubberlinePath.setLatLngs (this._polylineArc (lastCircleCoords, mouseCoords)); 823 | var currentTooltip = this._currentLine.tooltips.last(); 824 | var prevTooltip = this._currentLine.tooltips.slice(-2,-1)[0]; 825 | currentTooltip.setLatLng (mouseCoords); 826 | var distanceSegment = mouseCoords.distanceTo (lastCircleCoords); 827 | this._updateTooltip (currentTooltip, prevTooltip, this._currentLine.distance + distanceSegment, distanceSegment, lastCircleCoords, mouseCoords); 828 | }, 829 | 830 | _startLine: function (clickCoords) { 831 | var icon = L.divIcon({ 832 | className: 'polyline-measure-tooltip', 833 | iconAnchor: [-4, -4] 834 | }); 835 | var last = function() { 836 | return this.slice(-1)[0]; 837 | }; 838 | this._rubberlinePath = L.polyline ([], { 839 | // Style of temporary, dashed line while moving the mouse 840 | color: this.options.tempLine.color, 841 | weight: this.options.tempLine.weight, 842 | interactive: false, 843 | dashArray: '8,8' 844 | }).addTo(this._layerPaint).bringToBack(); 845 | 846 | var polylineState = this; // use "polylineState" instead of "this" to allow measuring on 2 different maps the same time 847 | 848 | this._currentLine = { 849 | id: 0, 850 | circleCoords: [], 851 | circleMarkers: [], 852 | arrowMarkers: [], 853 | tooltips: [], 854 | distance: 0, 855 | 856 | polylinePath: L.polyline([], { 857 | // Style of fixed, polyline after mouse is clicked 858 | color: this.options.fixedLine.color, 859 | weight: this.options.fixedLine.weight, 860 | interactive: false 861 | }).addTo(this._layerPaint).bringToBack(), 862 | 863 | handleMarkers: function (latlng) { 864 | // update style on previous marker 865 | var lastCircleMarker = this.circleMarkers.last(); 866 | if (lastCircleMarker) { 867 | lastCircleMarker.bindTooltip (polylineState.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 868 | lastCircleMarker.off ('click', polylineState._finishPolylinePath, polylineState); 869 | if (this.circleMarkers.length === 1) { 870 | lastCircleMarker.setStyle (polylineState.options.startCircle); 871 | } else { 872 | lastCircleMarker.setStyle (polylineState.options.intermedCircle); 873 | } 874 | } 875 | var newCircleMarker = new L.CircleMarker (latlng, polylineState.options.currentCircle).addTo(polylineState._layerPaint); 876 | newCircleMarker.bindTooltip (polylineState.options.tooltipTextFinish + polylineState.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 877 | newCircleMarker.cntLine = polylineState._currentLine.id; 878 | newCircleMarker.cntCircle = polylineState._cntCircle; 879 | polylineState._cntCircle++; 880 | newCircleMarker.on ('mousedown', polylineState._dragCircle, polylineState); 881 | newCircleMarker.on ('click', polylineState._finishPolylinePath, polylineState); 882 | this.circleMarkers.push (newCircleMarker); 883 | }, 884 | 885 | getNewToolTip: function(latlng) { 886 | return L.marker (latlng, { 887 | icon: icon, 888 | interactive: false 889 | }); 890 | }, 891 | 892 | addPoint: function (mouseCoords) { 893 | var lastCircleCoords = this.circleCoords.last(); 894 | if (lastCircleCoords && lastCircleCoords.equals (mouseCoords)) { // don't add a new circle if the click was onto the last circle 895 | return; 896 | } 897 | this.circleCoords.push (mouseCoords); 898 | // update polyline 899 | if (this.circleCoords.length > 1) { 900 | var arc = polylineState._polylineArc (lastCircleCoords, mouseCoords); 901 | var arrowMarker = polylineState._drawArrow (arc); 902 | if (this.circleCoords.length > 2) { 903 | arc.shift(); // remove first coordinate of the arc, cause it is identical with last coordinate of previous arc 904 | } 905 | this.polylinePath.setLatLngs (this.polylinePath.getLatLngs().concat(arc)); 906 | // following lines needed especially for Mobile Browsers where we just use mouseclicks. No mousemoves, no tempLine. 907 | arrowMarker.cntLine = polylineState._currentLine.id; 908 | arrowMarker.cntArrow = polylineState._cntCircle - 1; 909 | polylineState._currentLine.arrowMarkers.push (arrowMarker); 910 | var distanceSegment = lastCircleCoords.distanceTo (mouseCoords); 911 | this.distance += distanceSegment; 912 | var currentTooltip = polylineState._currentLine.tooltips.last(); 913 | var prevTooltip = polylineState._currentLine.tooltips.slice(-1,-2)[0]; 914 | polylineState._updateTooltip (currentTooltip, prevTooltip, this.distance, distanceSegment, lastCircleCoords, mouseCoords); 915 | } 916 | // update last tooltip with final value 917 | if (currentTooltip) { 918 | currentTooltip.setLatLng (mouseCoords); 919 | } 920 | // add new tooltip to update on mousemove 921 | var tooltipNew = this.getNewToolTip(mouseCoords); 922 | tooltipNew.addTo(polylineState._layerPaint); 923 | this.tooltips.push (tooltipNew); 924 | this.handleMarkers (mouseCoords); 925 | }, 926 | 927 | finalize: function() { 928 | // remove tooltip created by last click 929 | polylineState._layerPaint.removeLayer (this.tooltips.last()); 930 | this.tooltips.pop(); 931 | // remove temporary rubberline 932 | polylineState._layerPaint.removeLayer (polylineState._rubberlinePath); 933 | if (this.circleCoords.length > 1) { 934 | this.tooltips.last()._icon.classList.add('polyline-measure-tooltip-end'); // add Class e.g. another background-color to the Previous Tooltip (which is the last fixed tooltip, cause the moving tooltip is being deleted later) 935 | var lastCircleMarker = this.circleMarkers.last() 936 | lastCircleMarker.setStyle (polylineState.options.endCircle); 937 | // use Leaflet's own tooltip method to shwo a popuo tooltip if user hovers the last circle of a polyline 938 | lastCircleMarker.unbindTooltip (); // to close the opened Tooltip after it's been opened after click onto point to finish the line 939 | polylineState._currentLine.circleMarkers.map (function (circle) {circle.bindTooltip (polylineState.options.tooltipTextMove + polylineState.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'})}); 940 | polylineState._currentLine.circleMarkers[0].bindTooltip (polylineState.options.tooltipTextMove + polylineState.options.tooltipTextDelete + polylineState.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 941 | lastCircleMarker.bindTooltip (polylineState.options.tooltipTextMove + polylineState.options.tooltipTextDelete + polylineState.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 942 | polylineState._currentLine.arrowMarkers.map (function (arrow) {arrow.bindTooltip (polylineState.options.tooltipTextAdd, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'})}); 943 | lastCircleMarker.off ('click', polylineState._finishPolylinePath, polylineState); 944 | lastCircleMarker.on ('click', polylineState._resumePolylinePath, polylineState); 945 | polylineState._arrPolylines [this.id] = this; 946 | } else { 947 | // if there is only one point, just clean it up 948 | polylineState._layerPaint.removeLayer (this.circleMarkers.last()); 949 | polylineState._layerPaint.removeLayer (this.tooltips.last()); 950 | } 951 | polylineState._resetPathVariables(); 952 | } 953 | }; 954 | 955 | var firstTooltip = L.marker (clickCoords, { 956 | icon: icon, 957 | interactive: false 958 | }) 959 | firstTooltip.addTo(this._layerPaint); 960 | var text = ''; 961 | if (this.options.showBearings === true) { 962 | text = this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; 963 | } 964 | text = text + '
+' + '0
'; 965 | text = text + '
' + '0
'; 966 | firstTooltip._icon.innerHTML = text; 967 | this._currentLine.tooltips.push (firstTooltip); 968 | this._currentLine.circleCoords.last = last; 969 | this._currentLine.tooltips.last = last; 970 | this._currentLine.circleMarkers.last = last; 971 | this._currentLine.id = this._arrPolylines.length; 972 | }, 973 | 974 | /** 975 | * Event to fire on mouse click 976 | * @param {Object} e Event 977 | * @private 978 | */ 979 | _mouseClick: function (e) { 980 | // skip if there are no coords provided by the event, or this event's screen coordinates match those of finishing CircleMarker for the line we just completed 981 | if (!e.latlng || (this._finishCircleScreencoords && this._finishCircleScreencoords.equals(e.containerPoint))) { 982 | return; 983 | } 984 | 985 | if (!this._currentLine && !this._mapdragging) { 986 | this._startLine (e.latlng); 987 | this._map.fire('polylinemeasure:start', this._currentLine); 988 | } 989 | // just create a point if the map isn't dragged during the mouseclick. 990 | if (!this._mapdragging) { 991 | this._currentLine.addPoint (e.latlng); 992 | this._map.fire('polylinemeasure:add', e); 993 | this._map.fire('polylinemeasure:change', this._currentLine); 994 | } else { 995 | this._mapdragging = false; // this manual setting to "false" needed, instead of a "moveend"-Event. Cause the mouseclick of a "moveend"-event immediately would create a point too the same time. 996 | } 997 | }, 998 | 999 | /** 1000 | * Finish the drawing of the path by clicking onto the last circle or pressing ESC-Key 1001 | * @private 1002 | */ 1003 | _finishPolylinePath: function (e) { 1004 | this._map.fire('polylinemeasure:finish', this._currentLine); 1005 | this._currentLine.finalize(); 1006 | if (e) { 1007 | this._finishCircleScreencoords = e.containerPoint; 1008 | } 1009 | }, 1010 | 1011 | /** 1012 | * Resume the drawing of a polyline by pressing CTRL-Key and clicking onto the last circle 1013 | * @private 1014 | */ 1015 | _resumePolylinePath: function (e) { 1016 | if (e.originalEvent.ctrlKey === true || e.originalEvent.metaKey === true) { // just resume if user pressed the CTRL-Key (or metaKey on Mac) while clicking onto the last circle 1017 | this._currentLine = this._arrPolylines [e.target.cntLine]; 1018 | this._rubberlinePath = L.polyline ([], { 1019 | // Style of temporary, rubberline while moving the mouse 1020 | color: this.options.tempLine.color, 1021 | weight: this.options.tempLine.weight, 1022 | interactive: false, 1023 | dashArray: '8,8' 1024 | }).addTo(this._layerPaint).bringToBack(); 1025 | this._currentLine.tooltips.last()._icon.classList.remove ('polyline-measure-tooltip-end'); // remove extra CSS-class of previous, last tooltip 1026 | var tooltipNew = this._currentLine.getNewToolTip (e.latlng); 1027 | tooltipNew.addTo (this._layerPaint); 1028 | this._currentLine.tooltips.push(tooltipNew); 1029 | this._currentLine.circleMarkers.last().unbindTooltip(); // remove popup-tooltip of previous, last circleMarker 1030 | this._currentLine.circleMarkers.last().bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1031 | this._currentLine.circleMarkers.last().setStyle (this.options.currentCircle); 1032 | this._cntCircle = this._currentLine.circleCoords.length; 1033 | this._map.fire('polylinemeasure:resume', this._currentLine); 1034 | } 1035 | }, 1036 | 1037 | /** 1038 | * After completing a path, reset all the values to prepare in creating the next polyline measurement 1039 | * @private 1040 | */ 1041 | _resetPathVariables: function() { 1042 | this._cntCircle = 0; 1043 | this._currentLine = null; 1044 | }, 1045 | 1046 | _clickedArrow: function(e) { 1047 | if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { // (metaKey for Mac) 1048 | var lineNr = e.target.cntLine; 1049 | var arrowNr = e.target.cntArrow; 1050 | this._arrPolylines[lineNr].arrowMarkers [arrowNr].removeFrom (this._layerPaint); 1051 | var newCircleMarker = new L.CircleMarker (e.latlng, this.options.intermedCircle).addTo(this._layerPaint); 1052 | newCircleMarker.cntLine = lineNr; 1053 | newCircleMarker.on ('mousedown', this._dragCircle, this); 1054 | newCircleMarker.bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1055 | this._arrPolylines[lineNr].circleMarkers.splice (arrowNr+1, 0, newCircleMarker); 1056 | this._arrPolylines[lineNr].circleMarkers.map (function (item, index) { 1057 | item.cntCircle = index; 1058 | }); 1059 | this._arrPolylines[lineNr].circleCoords.splice (arrowNr+1, 0, e.latlng); 1060 | var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); // get Coords of each Point of the current Polyline 1061 | var arc1 = this._polylineArc (this._arrPolylines[lineNr].circleCoords[arrowNr], e.latlng); 1062 | arc1.pop(); 1063 | var arc2 = this._polylineArc (e.latlng, this._arrPolylines[lineNr].circleCoords[arrowNr+2]); 1064 | Array.prototype.splice.apply (lineCoords, [(arrowNr)*(this._arcpoints-1), this._arcpoints].concat (arc1, arc2)); 1065 | this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); 1066 | var arrowMarker = this._drawArrow (arc1); 1067 | this._arrPolylines[lineNr].arrowMarkers[arrowNr] = arrowMarker; 1068 | arrowMarker = this._drawArrow (arc2); 1069 | this._arrPolylines[lineNr].arrowMarkers.splice(arrowNr+1,0,arrowMarker); 1070 | this._arrPolylines[lineNr].arrowMarkers.map (function (item, index) { 1071 | item.cntLine = lineNr; 1072 | item.cntArrow = index; 1073 | }); 1074 | this._tooltipNew = L.marker (e.latlng, { 1075 | icon: L.divIcon({ 1076 | className: 'polyline-measure-tooltip', 1077 | iconAnchor: [-4, -4] 1078 | }), 1079 | interactive: false 1080 | }); 1081 | this._tooltipNew.addTo(this._layerPaint); 1082 | this._arrPolylines[lineNr].tooltips.splice (arrowNr+1, 0, this._tooltipNew); 1083 | var totalDistance = 0; 1084 | this._arrPolylines[lineNr].tooltips.map (function (item, index) { 1085 | if (index >= 1) { 1086 | var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); 1087 | var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; 1088 | var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; 1089 | totalDistance += distance; 1090 | var prevTooltip = this._arrPolylines[lineNr].tooltips[index-1] 1091 | this._updateTooltip (item, prevTooltip, totalDistance, distance, lastCircleCoords, mouseCoords); 1092 | } 1093 | }.bind(this)); 1094 | this._map.fire('polylinemeasure:insert', e); 1095 | this._map.fire('polylinemeasure:change', this._arrPolylines[this._lineNr]); 1096 | } 1097 | }, 1098 | 1099 | _dragCircleMouseup: function () { 1100 | // bind new popup-tooltip to the last CircleMArker if dragging finished 1101 | if ((this._circleNr === 0) || (this._circleNr === this._arrPolylines[this._lineNr].circleCoords.length-1)) { 1102 | this._e1.target.bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1103 | } else { 1104 | this._e1.target.bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1105 | } 1106 | this._resetPathVariables(); 1107 | this._map.off ('mousemove', this._dragCircleMousemove, this); 1108 | this._map.dragging.enable(); 1109 | this._map.on ('mousemove', this._mouseMove, this); 1110 | this._map.off ('mouseup', this._dragCircleMouseup, this); 1111 | this._map.fire('polylinemeasure:move', this._e1); 1112 | this._map.fire('polylinemeasure:change', this._arrPolylines[this._lineNr]); 1113 | }, 1114 | 1115 | _dragCircleMousemove: function (e2) { 1116 | var mouseNewLat = e2.latlng.lat; 1117 | var mouseNewLng = e2.latlng.lng; 1118 | var latDifference = mouseNewLat - this._mouseStartingLat; 1119 | var lngDifference = mouseNewLng - this._mouseStartingLng; 1120 | var currentCircleCoords = L.latLng (this._circleStartingLat + latDifference, this._circleStartingLng + lngDifference); 1121 | var arcpoints = this._arcpoints; 1122 | var lineNr = this._e1.target.cntLine; 1123 | this._lineNr = lineNr; 1124 | var circleNr = this._e1.target.cntCircle; 1125 | this._circleNr = circleNr; 1126 | this._e1.target.setLatLng (currentCircleCoords); 1127 | this._e1.target.unbindTooltip(); // unbind popup-tooltip cause otherwise it would be annoying during dragging, or popup instantly again if it's just closed 1128 | this._arrPolylines[lineNr].circleCoords[circleNr] = currentCircleCoords; 1129 | var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); // get Coords of each Point of the current Polyline 1130 | if (circleNr >= 1) { // redraw previous arc just if circle is not starting circle of polyline 1131 | var newLineSegment1 = this._polylineArc(this._arrPolylines[lineNr].circleCoords[circleNr-1], currentCircleCoords); 1132 | // the next line's syntax has to be used since Internet Explorer doesn't know new spread operator (...) for inserting the individual elements of an array as 3rd argument of the splice method; Otherwise we could write: lineCoords.splice (circleNr*(arcpoints-1), arcpoints, ...newLineSegment1); 1133 | Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), arcpoints].concat (newLineSegment1)); 1134 | var arrowMarker = this._drawArrow (newLineSegment1); 1135 | arrowMarker.cntLine = lineNr; 1136 | arrowMarker.cntArrow = circleNr-1; 1137 | this._arrPolylines[lineNr].arrowMarkers [circleNr-1].removeFrom (this._layerPaint); 1138 | this._arrPolylines[lineNr].arrowMarkers [circleNr-1] = arrowMarker; 1139 | } 1140 | if (circleNr < this._arrPolylines[lineNr].circleCoords.length-1) { // redraw following arc just if circle is not end circle of polyline 1141 | var newLineSegment2 = this._polylineArc (currentCircleCoords, this._arrPolylines[lineNr].circleCoords[circleNr+1]); 1142 | Array.prototype.splice.apply (lineCoords, [circleNr*(arcpoints-1), arcpoints].concat (newLineSegment2)); 1143 | arrowMarker = this._drawArrow (newLineSegment2); 1144 | arrowMarker.cntLine = lineNr; 1145 | arrowMarker.cntArrow = circleNr; 1146 | this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); 1147 | this._arrPolylines[lineNr].arrowMarkers [circleNr] = arrowMarker; 1148 | } 1149 | this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); 1150 | if (circleNr >= 0) { // just update tooltip position if moved circle is 2nd, 3rd, 4th etc. circle of a polyline 1151 | this._arrPolylines[lineNr].tooltips[circleNr].setLatLng (currentCircleCoords); 1152 | } 1153 | var totalDistance = 0; 1154 | // update tooltip texts of each tooltip 1155 | this._arrPolylines[lineNr].tooltips.map (function (item, index) { 1156 | if (index >= 1) { 1157 | var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); 1158 | var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; 1159 | var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; 1160 | totalDistance += distance; 1161 | this._arrPolylines[lineNr].distance = totalDistance; 1162 | var prevTooltip = this._arrPolylines[lineNr].tooltips[index-1] 1163 | this._updateTooltip (item, prevTooltip, totalDistance, distance, lastCircleCoords, mouseCoords); 1164 | } 1165 | }.bind(this)); 1166 | this._map.on ('mouseup', this._dragCircleMouseup, this); 1167 | }, 1168 | 1169 | _resumeFirstpointMousemove: function (e) { 1170 | var lineNr = this._lineNr; 1171 | this._map.on ('click', this._resumeFirstpointClick, this); // necassary for _dragCircle. If switched on already within _dragCircle an unwanted click is fired at the end of the drag. 1172 | var mouseCoords = e.latlng; 1173 | this._rubberlinePath2.setLatLngs (this._polylineArc (mouseCoords, currentCircleCoords)); 1174 | this._tooltipNew.setLatLng (mouseCoords); 1175 | var totalDistance = 0; 1176 | var distance = mouseCoords.distanceTo (this._arrPolylines[lineNr].circleCoords[0]); 1177 | var lastCircleCoords = mouseCoords; 1178 | var currentCoords = this._arrPolylines[lineNr].circleCoords[0]; 1179 | totalDistance += distance; 1180 | var prevTooltip = this._tooltipNew; 1181 | var currentTooltip = this._arrPolylines[lineNr].tooltips[0] 1182 | this._updateTooltip (currentTooltip, prevTooltip, totalDistance, distance, lastCircleCoords, currentCoords); 1183 | this._arrPolylines[lineNr].tooltips.map (function (item, index) { 1184 | if (index >= 1) { 1185 | var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); 1186 | var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; 1187 | var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; 1188 | totalDistance += distance; 1189 | var prevTooltip = this._arrPolylines[lineNr].tooltips[index-1] 1190 | this._updateTooltip (item, prevTooltip, totalDistance, distance, lastCircleCoords, mouseCoords); 1191 | } 1192 | }.bind (this)); 1193 | }, 1194 | 1195 | _resumeFirstpointClick: function (e) { 1196 | var lineNr = this._lineNr; 1197 | this._resumeFirstpointFlag = false; 1198 | this._map.off ('mousemove', this._resumeFirstpointMousemove, this); 1199 | this._map.off ('click', this._resumeFirstpointClick, this); 1200 | this._layerPaint.removeLayer (this._rubberlinePath2); 1201 | this._arrPolylines[lineNr].circleMarkers [0].setStyle (this.options.intermedCircle); 1202 | this._arrPolylines[lineNr].circleMarkers [0].unbindTooltip(); 1203 | this._arrPolylines[lineNr].circleMarkers [0].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1204 | var newCircleMarker = new L.CircleMarker (e.latlng, this.options.startCircle).addTo(this._layerPaint); 1205 | newCircleMarker.cntLine = lineNr; 1206 | newCircleMarker.cntCircle = 0; 1207 | newCircleMarker.on ('mousedown', this._dragCircle, this); 1208 | newCircleMarker.bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1209 | this._arrPolylines[lineNr].circleMarkers.unshift(newCircleMarker); 1210 | this._arrPolylines[lineNr].circleMarkers.map (function (item, index) { 1211 | item.cntCircle = index; 1212 | }); 1213 | this._arrPolylines[lineNr].circleCoords.unshift(e.latlng); 1214 | var arc = this._polylineArc (e.latlng, currentCircleCoords); 1215 | var arrowMarker = this._drawArrow (arc); 1216 | this._arrPolylines[lineNr].arrowMarkers.unshift(arrowMarker); 1217 | this._arrPolylines[lineNr].arrowMarkers.map (function (item, index) { 1218 | item.cntLine = lineNr; 1219 | item.cntArrow = index; 1220 | }); 1221 | arc.pop(); // remove last coordinate of arc, cause it's already part of the next arc. 1222 | this._arrPolylines[lineNr].polylinePath.setLatLngs (arc.concat(this._arrPolylines[lineNr].polylinePath.getLatLngs())); 1223 | this._arrPolylines[lineNr].tooltips.unshift(this._tooltipNew); 1224 | this._map.on ('mousemove', this._mouseMove, this); 1225 | }, 1226 | 1227 | 1228 | // not just used for dragging Cirles but also for deleting circles and resuming line at its starting point. 1229 | _dragCircle: function (e1) { 1230 | var arcpoints = this._arcpoints; 1231 | if (e1.originalEvent.ctrlKey || e1.originalEvent.metaKey) { // if user wants to resume drawing a line. metaKey for Mac 1232 | this._map.off ('click', this._mouseClick, this); // to avoid unwanted creation of a new line if CTRL-clicked onto a point 1233 | // if user wants resume the line at its starting point 1234 | if (e1.target.cntCircle === 0) { 1235 | this._resumeFirstpointFlag = true; 1236 | this._lineNr = e1.target.cntLine; 1237 | var lineNr = this._lineNr; 1238 | this._circleNr = e1.target.cntCircle; 1239 | currentCircleCoords = e1.latlng; 1240 | this._arrPolylines[lineNr].circleMarkers [0].setStyle (this.options.currentCircle); 1241 | this._rubberlinePath2 = L.polyline ([], { 1242 | // Style of temporary, rubberline while moving the mouse 1243 | color: this.options.tempLine.color, 1244 | weight: this.options.tempLine.weight, 1245 | interactive: false, 1246 | dashArray: '8,8' 1247 | }).addTo(this._layerPaint).bringToBack(); 1248 | this._tooltipNew = L.marker (currentCircleCoords, { 1249 | icon: L.divIcon({ 1250 | className: 'polyline-measure-tooltip', 1251 | iconAnchor: [-4, -4] 1252 | }), 1253 | interactive: false 1254 | }); 1255 | this._tooltipNew.addTo(this._layerPaint); 1256 | var text=''; 1257 | if (this.options.showBearings === true) { 1258 | text = text + this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; 1259 | } 1260 | text = text + '
+' + '0
'; 1261 | text = text + '
' + '0
'; 1262 | this._tooltipNew._icon.innerHTML = text; 1263 | this._map.off ('mousemove', this._mouseMove, this); 1264 | this._map.on ('mousemove', this._resumeFirstpointMousemove, this); 1265 | } 1266 | return; 1267 | } 1268 | 1269 | // if user wants to delete a circle 1270 | if (e1.originalEvent.shiftKey) { // it's not possible to use "ALT-Key" instead, cause this won't work in some Linux distributions (there it's the default hotkey for moving windows) 1271 | this._lineNr = e1.target.cntLine; 1272 | var lineNr = this._lineNr; 1273 | this._circleNr = e1.target.cntCircle; 1274 | var circleNr = this._circleNr; 1275 | 1276 | // if there is a rubberlinePath-layer and rubberline-id = clicked line-id of point meaning user is deleting a point of current line being drawn 1277 | if ((this._layerPaint.hasLayer (this._rubberlinePath)) && (lineNr === this._currentLine.id)) { 1278 | // when you're drawing and deleting point you need to take it into account by decreasing _cntCircle 1279 | this._cntCircle--; 1280 | // if the last Circle in polyline is being removed 1281 | if(this._currentLine.circleMarkers.length === 1) { 1282 | this._currentLine.finalize(); 1283 | return; 1284 | } 1285 | 1286 | this._currentLine.circleCoords.splice(circleNr,1); 1287 | this._currentLine.circleMarkers [circleNr].removeFrom (this._layerPaint); 1288 | this._currentLine.circleMarkers.splice(circleNr,1); 1289 | this._currentLine.circleMarkers.map (function (item, index) { 1290 | item.cntCircle = index; 1291 | }); 1292 | lineCoords = this._currentLine.polylinePath.getLatLngs(); 1293 | this._currentLine.tooltips [circleNr].removeFrom (this._layerPaint); 1294 | this._currentLine.tooltips.splice(circleNr,1); 1295 | 1296 | // if first Circle is being removed 1297 | if (circleNr === 0) { 1298 | if(this._currentLine.circleMarkers.length === 1) { 1299 | this._currentLine.circleMarkers [0].setStyle (this.options.currentCircle); 1300 | } else { 1301 | this._currentLine.circleMarkers [0].setStyle (this.options.startCircle); 1302 | } 1303 | lineCoords.splice (0, arcpoints-1); 1304 | this._currentLine.circleMarkers [0].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1305 | this._currentLine.arrowMarkers [circleNr].removeFrom (this._layerPaint); 1306 | this._currentLine.arrowMarkers.splice(0,1); 1307 | var text=''; 1308 | if (this.options.showBearings === true) { 1309 | text = this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; 1310 | } 1311 | text = text + '
+' + '0
'; 1312 | text = text + '
' + '0
'; 1313 | this._currentLine.tooltips [0]._icon.innerHTML = text; 1314 | // if last Circle is being removed 1315 | } else if (circleNr === this._currentLine.circleCoords.length) { 1316 | this._currentLine.circleMarkers [circleNr-1].on ('click', this._resumePolylinePath, this); 1317 | this._currentLine.circleMarkers [circleNr-1].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1318 | this._currentLine.circleMarkers.slice(-1)[0].setStyle (this.options.currentCircle); // get last element of the array 1319 | lineCoords.splice (-(arcpoints-1), arcpoints-1); 1320 | this._currentLine.arrowMarkers [circleNr-1].removeFrom (this._layerPaint); 1321 | this._currentLine.arrowMarkers.splice(-1,1); 1322 | // if intermediate Circle is being removed 1323 | } else { 1324 | newLineSegment = this._polylineArc (this._currentLine.circleCoords[circleNr-1], this._currentLine.circleCoords[circleNr]); 1325 | Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), (2*arcpoints-1)].concat (newLineSegment)); 1326 | this._currentLine.arrowMarkers [circleNr-1].removeFrom (this._layerPaint); 1327 | this._currentLine.arrowMarkers [circleNr].removeFrom (this._layerPaint); 1328 | arrowMarker = this._drawArrow (newLineSegment); 1329 | this._currentLine.arrowMarkers.splice(circleNr-1,2,arrowMarker); 1330 | } 1331 | this._currentLine.polylinePath.setLatLngs (lineCoords); 1332 | this._currentLine.arrowMarkers.map (function (item, index) { 1333 | item.cntLine = lineNr; 1334 | item.cntArrow = index; 1335 | }); 1336 | var totalDistanceUnfinishedLine = 0; 1337 | this._currentLine.tooltips.map (function (item, index, arr) { 1338 | if (index >= 1) { 1339 | var distance, mouseCoords; 1340 | var prevTooltip = this._currentLine.tooltips[index-1]; 1341 | var lastCircleCoords = this._currentLine.circleCoords[index - 1]; 1342 | if(index === arr.length - 1) { 1343 | distance = this._currentLine.circleCoords[index-1].distanceTo (e1.latlng); 1344 | mouseCoords = e1.latlng; 1345 | // if this is the last Circle (mouse cursor) then don't sum the distance, but update tooltip like it was summed 1346 | this._updateTooltip (item, prevTooltip, totalDistanceUnfinishedLine + distance, distance, lastCircleCoords, mouseCoords); 1347 | } else { 1348 | distance = this._currentLine.circleCoords[index-1].distanceTo (this._currentLine.circleCoords[index]); 1349 | mouseCoords = this._currentLine.circleCoords[index]; 1350 | // if this is not the last Circle (mouse cursor) then sum the distance 1351 | totalDistanceUnfinishedLine += distance; 1352 | this._updateTooltip (item, prevTooltip, totalDistanceUnfinishedLine, distance, lastCircleCoords, mouseCoords); 1353 | } 1354 | } 1355 | }.bind (this)); 1356 | 1357 | // update _currentLine distance after point deletion 1358 | this._currentLine.distance = totalDistanceUnfinishedLine; 1359 | } else { 1360 | if (this._arrPolylines[lineNr].circleMarkers.length === 2) { // if there are just 2 remaining points, delete all these points and the remaining line, since there should not stay a lonely point the map 1361 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].circleMarkers [1]); 1362 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].tooltips [1]); 1363 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].circleMarkers [0]); 1364 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].tooltips [0]); 1365 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].arrowMarkers [0]); 1366 | this._layerPaint.removeLayer (this._arrPolylines[lineNr].polylinePath); 1367 | this._map.fire('polylinemeasure:remove', e1); 1368 | this._map.fire('polylinemeasure:change', this._arrPolylines[this._lineNr]); 1369 | return; 1370 | } 1371 | 1372 | this._arrPolylines[lineNr].circleCoords.splice(circleNr,1); 1373 | this._arrPolylines[lineNr].circleMarkers [circleNr].removeFrom (this._layerPaint); 1374 | this._arrPolylines[lineNr].circleMarkers.splice(circleNr,1); 1375 | this._arrPolylines[lineNr].circleMarkers.map (function (item, index) { 1376 | item.cntCircle = index; 1377 | }); 1378 | var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); 1379 | this._arrPolylines[lineNr].tooltips [circleNr].removeFrom (this._layerPaint); 1380 | this._arrPolylines[lineNr].tooltips.splice(circleNr,1); 1381 | 1382 | // if the last Circle in polyline is being removed (in the code above, so length will be equal 0) 1383 | if(!this._arrPolylines[lineNr].circleMarkers.length) { 1384 | this._arrPolylines.splice(lineNr, 1); 1385 | // when you delete the line in the middle of array, other lines indexes change, so you need to update line number of markers and circles 1386 | this._arrPolylines.forEach(function(line, index) { 1387 | line.circleMarkers.map(function (item) { 1388 | item.cntLine = index; 1389 | }); 1390 | line.arrowMarkers.map(function (item) { 1391 | item.cntLine = index; 1392 | }); 1393 | }); 1394 | 1395 | return; 1396 | } 1397 | // if first Circle is being removed 1398 | if (circleNr === 0) { 1399 | this._arrPolylines[lineNr].circleMarkers [0].setStyle (this.options.startCircle); 1400 | lineCoords.splice (0, arcpoints-1); 1401 | this._arrPolylines[lineNr].circleMarkers [0].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1402 | this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); 1403 | this._arrPolylines[lineNr].arrowMarkers.splice(0,1); 1404 | var text=''; 1405 | if (this.options.showBearings === true) { 1406 | text = this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; 1407 | } 1408 | text = text + '
+' + '0
'; 1409 | text = text + '
' + '0
'; 1410 | this._arrPolylines[lineNr].tooltips [0]._icon.innerHTML = text; 1411 | // if last Circle is being removed 1412 | } else if (circleNr === this._arrPolylines[lineNr].circleCoords.length) { 1413 | this._arrPolylines[lineNr].circleMarkers [circleNr-1].on ('click', this._resumePolylinePath, this); 1414 | this._arrPolylines[lineNr].circleMarkers [circleNr-1].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); 1415 | this._arrPolylines[lineNr].circleMarkers.slice(-1)[0].setStyle (this.options.endCircle); // get last element of the array 1416 | this._arrPolylines[lineNr].tooltips.slice(-1)[0]._icon.classList.add('polyline-measure-tooltip-end'); 1417 | lineCoords.splice (-(arcpoints-1), arcpoints-1); 1418 | this._arrPolylines[lineNr].arrowMarkers [circleNr-1].removeFrom (this._layerPaint); 1419 | this._arrPolylines[lineNr].arrowMarkers.splice(-1,1); 1420 | // if intermediate Circle is being removed 1421 | } else { 1422 | var newLineSegment = this._polylineArc (this._arrPolylines[lineNr].circleCoords[circleNr-1], this._arrPolylines[lineNr].circleCoords[circleNr]); 1423 | Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), (2*arcpoints-1)].concat (newLineSegment)); 1424 | this._arrPolylines[lineNr].arrowMarkers [circleNr-1].removeFrom (this._layerPaint); 1425 | this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); 1426 | var arrowMarker = this._drawArrow (newLineSegment); 1427 | this._arrPolylines[lineNr].arrowMarkers.splice(circleNr-1,2,arrowMarker); 1428 | } 1429 | this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); 1430 | this._arrPolylines[lineNr].arrowMarkers.map (function (item, index) { 1431 | item.cntLine = lineNr; 1432 | item.cntArrow = index; 1433 | }); 1434 | var totalDistance = 0; 1435 | this._arrPolylines[lineNr].tooltips.map (function (item, index) { 1436 | if (index >= 1) { 1437 | var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); 1438 | var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; 1439 | var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; 1440 | totalDistance += distance; 1441 | this._arrPolylines[lineNr].distance = totalDistance; 1442 | var prevTooltip = this._arrPolylines[lineNr].tooltips[index-1]; 1443 | this._updateTooltip (item, prevTooltip, totalDistance, distance, lastCircleCoords, mouseCoords); 1444 | } 1445 | }.bind (this)); 1446 | // if user is deleting a point of a line not finished yet (= rubbberline still present) 1447 | } 1448 | 1449 | this._map.fire('polylinemeasure:remove', e1); 1450 | this._map.fire('polylinemeasure:change', this._arrPolylines[this._lineNr]); 1451 | return; 1452 | } 1453 | this._e1 = e1; 1454 | if ((this._measuring) && (this._cntCircle === 0)) { // just execute drag-function if Measuring tool is active but no line is being drawn at the moment. 1455 | this._map.dragging.disable(); // turn of moving of the map during drag of a circle 1456 | this._map.off ('mousemove', this._mouseMove, this); 1457 | this._map.off ('click', this._mouseClick, this); 1458 | this._mouseStartingLat = e1.latlng.lat; 1459 | this._mouseStartingLng = e1.latlng.lng; 1460 | this._circleStartingLat = e1.target._latlng.lat; 1461 | this._circleStartingLng = e1.target._latlng.lng; 1462 | this._map.on ('mousemove', this._dragCircleMousemove, this); 1463 | } 1464 | }, 1465 | 1466 | /** 1467 | * Takes in a dataset and programatically draws the polylines and measurements to the map 1468 | * Dataset must be in the form of an array of LatLng[], which allows for multiple discontinuous 1469 | * polylines to be seeded 1470 | * @param {L.LatLng[][]} polylinesArray | Array of array of points 1471 | */ 1472 | seed: function(polylinesArray){ 1473 | // Hijack user actions to manually draw polylines 1474 | polylinesArray.forEach((polyline) => { 1475 | // toggle draw state on: 1476 | this._toggleMeasure(); 1477 | // start line with first point of each polyline 1478 | this._startLine(polyline[0]); 1479 | // add subsequent points: 1480 | polyline.forEach((point, ind) => { 1481 | const latLng = L.latLng(point); 1482 | this._mouseMove({ latLng }); 1483 | this._currentLine.addPoint(latLng); 1484 | // on last point, 1485 | if (ind === polyline.length - 1) { 1486 | this._finishPolylinePath(); 1487 | this._toggleMeasure(); 1488 | } 1489 | }); 1490 | }); 1491 | } 1492 | }); 1493 | 1494 | //====================================================================================== 1495 | 1496 | L.Map.mergeOptions({ 1497 | PolylineMeasureControl: false 1498 | }); 1499 | 1500 | L.Map.addInitHook(function () { 1501 | if (this.options.polylineMeasureControl) { 1502 | this.PMControl = new L.Control.PolylineMeasure(); 1503 | this.addControl(this.PMControl); 1504 | } 1505 | }); 1506 | 1507 | L.control.polylineMeasure = function (options) { 1508 | return new L.Control.PolylineMeasure (options); 1509 | }; 1510 | 1511 | return L.Control.PolylineMeasure; 1512 | // to allow 1513 | // import PolylineMeasure from 'leaflet.polylinemeasure'; 1514 | // const measureControl = new PolylineMeasure(); 1515 | // together with 1516 | // import 'leaflet.polylinemeasure'; 1517 | // const measureControl = new L.Control.PolylineMeasure(); 1518 | 1519 | })); 1520 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Leaflet.PolylineMeasure 4 | * Leaflet Plugin to **measure distances** of simple lines as well as of complex polylines. 5 | * Measuring in **metric system** (metres, kilometres), in **imperial system** (feet, landmiles), or in **nautical miles**. 6 | * Lines are drawn as realistic arcs. **Bearings** and **distances** are calculated considering [**Great-circle distance**](https://en.wikipedia.org/wiki/Great-circle_distance) which is the shortest path between 2 points on Earth. 7 | * **Arrows** indicating the **real midways** of the line's great-circle **distances**, not their optical middle which is different due to projection, especially in high latitudes. 8 | * To **finish** drawing a line just *doubleclick*, or *singleclick* onto the last (=orange) point, or *press "ESC"-key*. 9 | * **Moving** of line's points afterwards is possible by clicking and draging them. *(This feature can not be guaranteed to work on every **mobile** browser using touch input, e.g. with Chrome Mobile it isn't working right now)* 10 | * To **continue** a line after it has been finished, hold the *Ctrl-Key* while clicking onto the first or last point of a line. 11 | * To **add** points, hold the *Ctrl-Key* while clicking onto an arrow. 12 | * To **delete** points, hold the *Shift-Key* while clicking onto a point. 13 | * It is an evolution of jtreml's Plugin [leaflet.measure](https://github.com/jtreml/leaflet.measure) since the original plugin hasn't been bugfixed for years. I modified it to work again with **Leaflet v1.0 and newer** (still runs with Leaflet v0.7) and added functional and optical improvements. 14 | 15 | ## Demos 16 | * Please take a look at these demos: 17 | - [**Demo 1**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo1.html) (kilometre units, bearings, with Clear Control and Unit Control buttons) 18 | - [**Demo 2**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo2.html) (landmile units, without bearings, without Unit Control button) 19 | - [**Demo 3**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo3.html) (nautical mile units, bearings, without Unit Control and Clear Control buttons) 20 | - [**Demo 4**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo4.html) (two maps) 21 | - [**Demo 5**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo5.html) (programatically providing polyline points - "Seeding Data") 22 | 23 | ![Screenshot](https://ppete2.github.io/Leaflet.PolylineMeasure/screenshot.jpg) 24 | 25 | ## Usage 26 | 27 | Add 2 code lines within your **HTML-file** to load the .css and .js files of the plugin: 28 | ```html 29 | 30 | 31 | ``` 32 | 33 | Add 1 code line within your **Javascript-file** to add the plugin's control into your Leaflet map. 34 | ```js 35 | L.control.polylineMeasure(options).addTo(map); 36 | ``` 37 | 38 | ## Package manager install 39 | 40 | It's possible to install and update the Plugin using package managers like `npm`. This feature has been added by other users. I'm not familiar nor responsible to keep these package manager installs up-to-date. If you notice such installs being outdated, feel free to provide a Pull request or contact one of the persons who introduced package manager installs, thanks. 41 | 42 | ## Default options 43 | 44 | ```js 45 | options = { 46 | position: 'topleft', // Position to show the control. Values: 'topright', 'topleft', 'bottomright', 'bottomleft' 47 | unit: 'kilometres', // Default unit the distances are displayed in. Values: 'kilometres', 'landmiles', 'nauticalmiles' 48 | useSubunits: true, // Use subunits (metres/feet) in tooltips if distances are less than 1 kilometre/landmile 49 | clearMeasurementsOnStop: true, // Clear all measurements when Measure Control is switched off 50 | showBearings: false, // Whether bearings are displayed within the tooltips 51 | bearingTextIn: 'In', // language dependend label for inbound bearings 52 | bearingTextOut: 'Out', // language dependend label for outbound bearings 53 | tooltipTextFinish: 'Click to finish line
', 54 | tooltipTextDelete: 'Press SHIFT-key and click to delete point', 55 | tooltipTextMove: 'Click and drag to move point
', 56 | tooltipTextResume: '
Press CTRL-key and click to resume line', 57 | tooltipTextAdd: 'Press CTRL-key and click to add point', 58 | // language dependend labels for point's tooltips 59 | measureControlTitleOn: 'Turn on PolylineMeasure', // Title for the Measure Control going to be switched on 60 | measureControlTitleOff: 'Turn off PolylineMeasure', // Title for the Measure Control going to be switched off 61 | measureControlLabel: '↦', // Label of the Measure Control (Unicode symbols are possible) 62 | measureControlClasses: [], // Classes to apply to the Measure Control 63 | showClearControl: false, // Show a control to clear all the measurements 64 | clearControlTitle: 'Clear Measurements', // Title text to show on the Clear Control 65 | clearControlLabel: '×', // Label of the Clear Control (Unicode symbols are possible) 66 | clearControlClasses: [], // Classes to apply to Clear Control 67 | showUnitControl: false, // Show a control to change the units of measurements 68 | unitControlUnits: ["kilometres", "landmiles", "nauticalmiles"], 69 | // measurement units being cycled through by using the Unit Control 70 | unitControlTitle: { // Title texts to show on the Unit Control 71 | text: 'Change Units', 72 | kilometres: 'kilometres', 73 | landmiles: 'land miles', 74 | nauticalmiles: 'nautical miles' 75 | }, 76 | unitControlLabel: { // Unit symbols to show in the Unit Control and measurement labels 77 | metres: 'm', 78 | kilometres: 'km', 79 | feet: 'ft', 80 | landmiles: 'mi', 81 | nauticalmiles: 'nm' 82 | }, 83 | unitControlClasses: [], // Classes to apply to the Unit Control 84 | tempLine: { // Styling settings for the temporary dashed line 85 | color: '#00f', // Dashed line color 86 | weight: 2 // Dashed line weight 87 | }, 88 | fixedLine: { // Styling for the solid line 89 | color: '#006', // Solid line color 90 | weight: 2 // Solid line weight 91 | }, 92 | arrow: { // Styling of the midway arrow 93 | color: '#000', // Color of the arrow 94 | }, 95 | startCircle: { // Style settings for circle marker indicating the starting point of the polyline 96 | color: '#000', // Color of the border of the circle 97 | weight: 1, // Weight of the circle 98 | fillColor: '#0f0', // Fill color of the circle 99 | fillOpacity: 1, // Fill opacity of the circle 100 | radius: 3 // Radius of the circle 101 | }, 102 | intermedCircle: { // Style settings for all circle markers between startCircle and endCircle 103 | color: '#000', // Color of the border of the circle 104 | weight: 1, // Weight of the circle 105 | fillColor: '#ff0', // Fill color of the circle 106 | fillOpacity: 1, // Fill opacity of the circle 107 | radius: 3 // Radius of the circle 108 | }, 109 | currentCircle: { // Style settings for circle marker indicating the latest point of the polyline during drawing a line 110 | color: '#000', // Color of the border of the circle 111 | weight: 1, // Weight of the circle 112 | fillColor: '#f0f', // Fill color of the circle 113 | fillOpacity: 1, // Fill opacity of the circle 114 | radius: 6 // Radius of the circle 115 | }, 116 | endCircle: { // Style settings for circle marker indicating the last point of the polyline 117 | color: '#000', // Color of the border of the circle 118 | weight: 1, // Weight of the circle 119 | fillColor: '#f00', // Fill color of the circle 120 | fillOpacity: 1, // Fill opacity of the circle 121 | radius: 3 // Radius of the circle 122 | }, 123 | }; 124 | ``` 125 | 126 | ## Events 127 | Several Events are fired during the use of the Plugin in order to offer interactivity outside the Plugin. 128 | Subscribe to events with: 129 | 130 | ```js 131 | map.on('polylinemeasure:toogle', e => { /* e.sttus */ }); 132 | map.on('polylinemeasure:start', currentLine => {...}); 133 | map.on('polylinemeasure:resume', currentLine => {...}); 134 | map.on('polylinemeasure:finish', currentLine => {...}); 135 | map.on('polylinemeasure:change', currentLine => {...}); 136 | map.on('polylinemeasure:clear', e => {...}); 137 | map.on('polylinemeasure:add', e => { /* e.latlng */ }); 138 | map.on('polylinemeasure:insert', e => { /* e.latlng */ }); 139 | map.on('polylinemeasure:move', e => { /* e.latlng ; e.sourceTarget._latlng */ }); 140 | map.on('polylinemeasure:remove', e => { /* e.latlng ; e.sourceTarget._latlng */ }); 141 | ``` 142 | * Please take a look at [**Demo 1**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo1.html), where those events get listed in the JS console of your browser 143 | 144 | ## Seeding Data 145 | You can programatically draw measured polylines using the `.seed` method. It takes an array of arrays of `L.LatLng`, which enables drawing multiple, discontinuous polylines: 146 | 147 | ```js 148 | let polylineMeasure = L.control.polylineMeasure(options); 149 | polylineMeasure.addTo (map); 150 | 151 | const line1coords = [ 152 | { lat: 22.156883186860703, lng: -158.95019531250003 }, 153 | { lat: 22.01436065310322, lng: -157.33520507812503 }, 154 | { lat: 21.391704731036587, lng: -156.17065429687503 }, 155 | { lat: 20.64306554672647, lng: -155.56640625000003 } 156 | ]; 157 | const line2coords = [ 158 | { lat: 19.880391767822505, lng: -159.67529296875003 }, 159 | { lat: 17.90556881196468, lng: -156.39038085937503 } 160 | ]; 161 | 162 | polylineMeasure.seed([line1coords, line2coords]) 163 | ``` 164 | 165 | * Please take a look at [**Demo 5**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo5.html), where multiple polylines are drawn and measured programatically. 166 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /demo1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 1 of Leaflet.PolylineMeasure 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 |
19 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /demo2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 2 of Leaflet.PolylineMeasure 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 |
19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 3 of Leaflet.PolylineMeasure 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 |
19 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /demo4.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Two Map Demo of Leaflet.PolylineMeasure 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
19 |
20 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demo5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 5 of Leaflet.PolylineMeasure 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 |
19 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet.polylinemeasure", 3 | "version": "1.0.0", 4 | "description": "Leaflet Plugin to measure distances of simple lines as well as of complex polylines", 5 | "main": "Leaflet.PolylineMeasure.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/ppete2/Leaflet.PolylineMeasure.git" 12 | }, 13 | "author": "", 14 | "license": "BSD-2-Clause", 15 | "bugs": { 16 | "url": "https://github.com/ppete2/Leaflet.PolylineMeasure/issues" 17 | }, 18 | "homepage": "https://github.com/ppete2/Leaflet.PolylineMeasure#readme" 19 | } 20 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ppete2/Leaflet.PolylineMeasure/9908e5a2cbd2753c335ac758f3559ba9da6f99e9/screenshot.jpg --------------------------------------------------------------------------------