From f5fe9d16d5271453798eceb5fc1cb39845b4961c Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Thu, 11 Jul 2024 17:59:59 +0100 Subject: [PATCH 1/7] Allow rhumblines to be used instead of great circle lines. --- Leaflet.PolylineMeasure.js | 218 +++++++++++++++++++++++++++++-------- README.md | 2 + demo6.html | 41 +++++++ 3 files changed, 213 insertions(+), 48 deletions(-) create mode 100644 demo6.html diff --git a/Leaflet.PolylineMeasure.js b/Leaflet.PolylineMeasure.js index 75fc6d9..b0d942e 100644 --- a/Leaflet.PolylineMeasure.js +++ b/Leaflet.PolylineMeasure.js @@ -66,7 +66,20 @@ * @default */ showBearings: false, - /** + /** + * Whether to use simple rhumb lines instead of great circle measurements + * @type {Boolean} + * @default + */ + useRhumbLines: false, + /** + * Number of points to draw in each arc (when using default great circle method) + * 99 points = 98 line segments. lower value to make arc less accurate or increase value to make it more accurate. + * @type {Int} + * @default + */ + arcpoints: 99, + /** * Text for the bearing In * @type {String} * @default @@ -373,7 +386,6 @@ } }, - _arcpoints: 99, // 99 points = 98 line segments. lower value to make arc less accurate or increase value to make it more accurate. _circleNr: -1, _lineNr: -1, @@ -670,6 +682,40 @@ return {value:dist, unit:unit}; }, + /** + * Determine points in each arc + * + * @returns {number} + * @private + */ + _arcPointsCnt: function() { + return this.options.useRhumbLines ? 2 : this.options.arcpoints; + }, + + /** + * Generate coordinates for line between two points + * + * @param _from + * @param _to + * @returns {*[]|*} + * @private + */ + _generateLineCoords: function (_from, _to) { + return this.options.useRhumbLines ? this._rhumbLineArc(_from, _to) : this._polylineArc(_from, _to); + }, + + /** + * Generate coordinates for line between two points using simple rhumbline + * + * @param _from + * @param _to + * @returns {[[(number|*),(number|*)],[(number|*),(number|*)]]} + * @private + */ + _rhumbLineArc: function (_from, _to) { + return [[_from.lat, _from.lng], [_to.lat, _to.lng]]; + }, + /** * Calculate Great-circle Arc (= shortest distance on a sphere like the Earth) between two coordinates * formulas: http://www.edwilliams.org/avform.htm @@ -724,20 +770,29 @@ if (d === 0) { arrLatLngs = [[fromLat, fromLng]]; } else { - arrLatLngs = _GCarc(this._arcpoints); + arrLatLngs = _GCarc(this._arcPointsCnt()); } return arrLatLngs; }, /** * Update the tooltip distance + * @TODO param details + * @param currentTooltip Current tooltip + * @param prevTooltip Previous tooltip * @param {Number} total Total distance * @param {Number} difference Difference in distance between 2 points + * @param lastCircleCoords Last circle coordinates + * @param mouseCoords Mouse coordinates * @private */ _updateTooltip: function (currentTooltip, prevTooltip, total, difference, lastCircleCoords, mouseCoords) { - // Explanation of formula: http://www.movable-type.co.uk/scripts/latlong.html - var calcAngle = function (p1, p2, direction) { + // Explanation of formulas: http://www.movable-type.co.uk/scripts/latlong.html + var calcAngle = function (p1, p2, direction, useRhumbLines) { + return useRhumbLines ? calcAngleRhumbLines(p1, p2, direction) : calcAngleGC(p1, p2, direction); + } + + var calcAngleGC = function (p1, p2, direction) { var lat1 = p1.lat / 180 * Math.PI; var lat2 = p2.lat / 180 * Math.PI; var lng1 = p1.lng / 180 * Math.PI; @@ -752,8 +807,38 @@ return (brng % 360); } - var angleIn = calcAngle (mouseCoords, lastCircleCoords, "inbound"); - var angleOut = calcAngle (lastCircleCoords, mouseCoords, "outbound"); + var calcAngleRhumbLines = function (p1, p2, direction) { + var lat1 = p1.lat / 180 * Math.PI; + var lat2 = p2.lat / 180 * Math.PI; + var lng1 = p1.lng / 180 * Math.PI; + var lng2 = p2.lng / 180 * Math.PI; + + var y = Math.log(Math.tan(Math.PI/4+lat2/2)/Math.tan(Math.PI/4+lat1/2)); + var x = lng2 - lng1; + + // if dLon over 180° take shorter rhumb line across the anti-meridian: + if (Math.abs(x) > Math.PI) x = x>0 ? -(2*Math.PI-x) : (2*Math.PI+x); + + if (direction === "inbound") { + var brng = Math.atan2(x, y) * 180/Math.PI - 180; + } else { + var brng = Math.atan2(x, y) * 180/Math.PI; + } + + // Fix 180-360º (westbound) bearings + if (brng < 0) brng = brng + 360; + + // Round value + brng = brng.toFixed(0); + + // If rounding to 360, use 0 instead + if (brng === "360") brng = 0; + + return brng; + } + + var angleIn = calcAngle (mouseCoords, lastCircleCoords, "inbound", this.options.useRhumbLines); + var angleOut = calcAngle (lastCircleCoords, mouseCoords, "outbound", this.options.useRhumbLines); var totalRound = this._getDistance (total); var differenceRound = this._getDistance (difference); var textCurrent = ''; @@ -774,7 +859,7 @@ }, _drawArrow: function (arcLine) { - // 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 + // 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 // 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 var midpoint = Math.trunc(arcLine.length/2); if (arcLine.length % 2 == 0) { @@ -782,7 +867,19 @@ var P2 = arcLine[midpoint]; var diffLng12 = P2[1] - P1[1]; var diffLat12 = P2[0] - P1[0]; - var center = [P1[0] + diffLat12/2, P1[1] + diffLng12/2]; + + if (this.options.useRhumbLines) { + // calculate midpoint using projected coords + var P1proj = L.CRS.EPSG3857.project(L.latLng(P1)); + var P2proj = L.CRS.EPSG3857.project(L.latLng(P2)); + var diffX12 = P2proj.x - P1proj.x; + var diffY12 = P2proj.y - P1proj.y; + var centerproj = [P1proj.x + diffX12/2, P1proj.y + diffY12/2]; + var center = L.CRS.EPSG3857.unproject(L.point(centerproj)); + } + else { + var center = [P1[0] + diffLat12/2, P1[1] + diffLng12/2]; + } } else { var P1 = arcLine[midpoint-1]; var P2 = arcLine[midpoint+1]; @@ -790,7 +887,7 @@ var diffLat12 = P2[0] - P1[0]; var center = arcLine[midpoint]; } - // 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 + // angle just an approximation, 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 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. var iconArrow = L.divIcon ({ className: "", // to avoid getting a default class with paddings and borders assigned by Leaflet @@ -819,7 +916,7 @@ return; } var lastCircleCoords = this._currentLine.circleCoords.last(); - this._rubberlinePath.setLatLngs (this._polylineArc (lastCircleCoords, mouseCoords)); + this._rubberlinePath.setLatLngs (this._generateLineCoords (lastCircleCoords, mouseCoords)); var currentTooltip = this._currentLine.tooltips.last(); var prevTooltip = this._currentLine.tooltips.slice(-2,-1)[0]; currentTooltip.setLatLng (mouseCoords); @@ -897,7 +994,7 @@ this.circleCoords.push (mouseCoords); // update polyline if (this.circleCoords.length > 1) { - var arc = polylineState._polylineArc (lastCircleCoords, mouseCoords); + var arc = polylineState._generateLineCoords (lastCircleCoords, mouseCoords); var arrowMarker = polylineState._drawArrow (arc); if (this.circleCoords.length > 2) { arc.shift(); // remove first coordinate of the arc, cause it is identical with last coordinate of previous arc @@ -1047,22 +1144,36 @@ if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { // (metaKey for Mac) var lineNr = e.target.cntLine; var arrowNr = e.target.cntArrow; + + // Remove arrow marker this._arrPolylines[lineNr].arrowMarkers [arrowNr].removeFrom (this._layerPaint); + + // Create new circle marker and add to map var newCircleMarker = new L.CircleMarker (e.latlng, this.options.intermedCircle).addTo(this._layerPaint); newCircleMarker.cntLine = lineNr; newCircleMarker.on ('mousedown', this._dragCircle, this); newCircleMarker.bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); + + // Add new circle marker to line this._arrPolylines[lineNr].circleMarkers.splice (arrowNr+1, 0, newCircleMarker); this._arrPolylines[lineNr].circleMarkers.map (function (item, index) { item.cntCircle = index; }); this._arrPolylines[lineNr].circleCoords.splice (arrowNr+1, 0, e.latlng); - var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); // get Coords of each Point of the current Polyline - var arc1 = this._polylineArc (this._arrPolylines[lineNr].circleCoords[arrowNr], e.latlng); - arc1.pop(); - var arc2 = this._polylineArc (e.latlng, this._arrPolylines[lineNr].circleCoords[arrowNr+2]); - Array.prototype.splice.apply (lineCoords, [(arrowNr)*(this._arcpoints-1), this._arcpoints].concat (arc1, arc2)); - this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); + + // get Coords of each Point of the current Polyline + var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); + // TODO: GC uses 0/1 whereas rhumbLines use lat/lng for some reason ... debug/standardise if necessary + //console.log('lineCoords 1', lineCoords); + + // generate coords up to this point + var arc1 = this._generateLineCoords (this._arrPolylines[lineNr].circleCoords[arrowNr], e.latlng); + + // generate coords from here to next circle + var arc2 = this._generateLineCoords (e.latlng, this._arrPolylines[lineNr].circleCoords[arrowNr+2]); + + // add new arrows + // NOTE: need to do this before removing last point from arc1, otherwise it breaks for rhumblines var arrowMarker = this._drawArrow (arc1); this._arrPolylines[lineNr].arrowMarkers[arrowNr] = arrowMarker; arrowMarker = this._drawArrow (arc2); @@ -1071,6 +1182,17 @@ item.cntLine = lineNr; item.cntArrow = index; }); + + // remove duplicated last point on first arc as it'll be replicated by second arc + arc1.pop(); + + // modify lineCoords array by removing a certain number of elements starting at a calculated index and + // then inserting new elements (arc1 and arc2) at that position + Array.prototype.splice.apply(lineCoords, [(arrowNr) * (this._arcPointsCnt() - 1), this._arcPointsCnt()].concat(arc1, arc2)); + + // update polylinePath with new lineCoords + this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); + this._tooltipNew = L.marker (e.latlng, { icon: L.divIcon({ className: 'polyline-measure-tooltip', @@ -1118,7 +1240,7 @@ var latDifference = mouseNewLat - this._mouseStartingLat; var lngDifference = mouseNewLng - this._mouseStartingLng; var currentCircleCoords = L.latLng (this._circleStartingLat + latDifference, this._circleStartingLng + lngDifference); - var arcpoints = this._arcpoints; + var arcpoints = this._arcPointsCnt(); var lineNr = this._e1.target.cntLine; this._lineNr = lineNr; var circleNr = this._e1.target.cntCircle; @@ -1128,7 +1250,7 @@ this._arrPolylines[lineNr].circleCoords[circleNr] = currentCircleCoords; var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); // get Coords of each Point of the current Polyline if (circleNr >= 1) { // redraw previous arc just if circle is not starting circle of polyline - var newLineSegment1 = this._polylineArc(this._arrPolylines[lineNr].circleCoords[circleNr-1], currentCircleCoords); + var newLineSegment1 = this._generateLineCoords(this._arrPolylines[lineNr].circleCoords[circleNr-1], currentCircleCoords); // 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); Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), arcpoints].concat (newLineSegment1)); var arrowMarker = this._drawArrow (newLineSegment1); @@ -1138,12 +1260,12 @@ this._arrPolylines[lineNr].arrowMarkers [circleNr-1] = arrowMarker; } if (circleNr < this._arrPolylines[lineNr].circleCoords.length-1) { // redraw following arc just if circle is not end circle of polyline - var newLineSegment2 = this._polylineArc (currentCircleCoords, this._arrPolylines[lineNr].circleCoords[circleNr+1]); + var newLineSegment2 = this._generateLineCoords (currentCircleCoords, this._arrPolylines[lineNr].circleCoords[circleNr+1]); Array.prototype.splice.apply (lineCoords, [circleNr*(arcpoints-1), arcpoints].concat (newLineSegment2)); - arrowMarker = this._drawArrow (newLineSegment2); + arrowMarker = this._drawArrow(newLineSegment2); arrowMarker.cntLine = lineNr; arrowMarker.cntArrow = circleNr; - this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); + this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom(this._layerPaint); this._arrPolylines[lineNr].arrowMarkers [circleNr] = arrowMarker; } this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); @@ -1170,7 +1292,7 @@ var lineNr = this._lineNr; 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. var mouseCoords = e.latlng; - this._rubberlinePath2.setLatLngs (this._polylineArc (mouseCoords, currentCircleCoords)); + this._rubberlinePath2.setLatLngs (this._generateLineCoords (mouseCoords, currentCircleCoords)); this._tooltipNew.setLatLng (mouseCoords); var totalDistance = 0; var distance = mouseCoords.distanceTo (this._arrPolylines[lineNr].circleCoords[0]); @@ -1211,7 +1333,7 @@ item.cntCircle = index; }); this._arrPolylines[lineNr].circleCoords.unshift(e.latlng); - var arc = this._polylineArc (e.latlng, currentCircleCoords); + var arc = this._generateLineCoords (e.latlng, currentCircleCoords); var arrowMarker = this._drawArrow (arc); this._arrPolylines[lineNr].arrowMarkers.unshift(arrowMarker); this._arrPolylines[lineNr].arrowMarkers.map (function (item, index) { @@ -1227,7 +1349,7 @@ // not just used for dragging Cirles but also for deleting circles and resuming line at its starting point. _dragCircle: function (e1) { - var arcpoints = this._arcpoints; + var arcpoints = this._arcPointsCnt(); if (e1.originalEvent.ctrlKey || e1.originalEvent.metaKey) { // if user wants to resume drawing a line. metaKey for Mac this._map.off ('click', this._mouseClick, this); // to avoid unwanted creation of a new line if CTRL-clicked onto a point // if user wants resume the line at its starting point @@ -1275,13 +1397,13 @@ // 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 if ((this._layerPaint.hasLayer (this._rubberlinePath)) && (lineNr === this._currentLine.id)) { - // when you're drawing and deleting point you need to take it into account by decreasing _cntCircle - this._cntCircle--; - // if the last Circle in polyline is being removed - if(this._currentLine.circleMarkers.length === 1) { - this._currentLine.finalize(); - return; - } + // when you're drawing and deleting point you need to take it into account by decreasing _cntCircle + this._cntCircle--; + // if the last Circle in polyline is being removed + if (this._currentLine.circleMarkers.length === 1) { + this._currentLine.finalize(); + return; + } this._currentLine.circleCoords.splice(circleNr,1); this._currentLine.circleMarkers [circleNr].removeFrom (this._layerPaint); @@ -1317,19 +1439,19 @@ this._currentLine.circleMarkers [circleNr-1].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); this._currentLine.circleMarkers.slice(-1)[0].setStyle (this.options.currentCircle); // get last element of the array lineCoords.splice (-(arcpoints-1), arcpoints-1); - this._currentLine.arrowMarkers [circleNr-1].removeFrom (this._layerPaint); - this._currentLine.arrowMarkers.splice(-1,1); + this._currentLine.arrowMarkers [circleNr - 1].removeFrom(this._layerPaint); + this._currentLine.arrowMarkers.splice(-1, 1); // if intermediate Circle is being removed } else { - newLineSegment = this._polylineArc (this._currentLine.circleCoords[circleNr-1], this._currentLine.circleCoords[circleNr]); + newLineSegment = this._generateLineCoords (this._currentLine.circleCoords[circleNr-1], this._currentLine.circleCoords[circleNr]); Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), (2*arcpoints-1)].concat (newLineSegment)); - this._currentLine.arrowMarkers [circleNr-1].removeFrom (this._layerPaint); - this._currentLine.arrowMarkers [circleNr].removeFrom (this._layerPaint); - arrowMarker = this._drawArrow (newLineSegment); - this._currentLine.arrowMarkers.splice(circleNr-1,2,arrowMarker); + this._currentLine.arrowMarkers [circleNr - 1].removeFrom(this._layerPaint); + this._currentLine.arrowMarkers [circleNr].removeFrom(this._layerPaint); + arrowMarker = this._drawArrow(newLineSegment); + this._currentLine.arrowMarkers.splice(circleNr - 1, 2, arrowMarker); } this._currentLine.polylinePath.setLatLngs (lineCoords); - this._currentLine.arrowMarkers.map (function (item, index) { + this._currentLine.arrowMarkers.map(function (item, index) { item.cntLine = lineNr; item.cntArrow = index; }); @@ -1399,8 +1521,8 @@ this._arrPolylines[lineNr].circleMarkers [0].setStyle (this.options.startCircle); lineCoords.splice (0, arcpoints-1); this._arrPolylines[lineNr].circleMarkers [0].bindTooltip (this.options.tooltipTextMove + this.options.tooltipTextDelete + this.options.tooltipTextResume, {direction:'top', opacity:0.7, className:'polyline-measure-popupTooltip'}); - this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); - this._arrPolylines[lineNr].arrowMarkers.splice(0,1); + this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom(this._layerPaint); + this._arrPolylines[lineNr].arrowMarkers.splice(0, 1); var text=''; if (this.options.showBearings === true) { text = this.options.bearingTextIn+':---°
'+this.options.bearingTextOut+':---°'; @@ -1415,19 +1537,19 @@ this._arrPolylines[lineNr].circleMarkers.slice(-1)[0].setStyle (this.options.endCircle); // get last element of the array this._arrPolylines[lineNr].tooltips.slice(-1)[0]._icon.classList.add('polyline-measure-tooltip-end'); lineCoords.splice (-(arcpoints-1), arcpoints-1); - this._arrPolylines[lineNr].arrowMarkers [circleNr-1].removeFrom (this._layerPaint); - this._arrPolylines[lineNr].arrowMarkers.splice(-1,1); + this._arrPolylines[lineNr].arrowMarkers [circleNr - 1].removeFrom(this._layerPaint); + this._arrPolylines[lineNr].arrowMarkers.splice(-1, 1); // if intermediate Circle is being removed } else { - var newLineSegment = this._polylineArc (this._arrPolylines[lineNr].circleCoords[circleNr-1], this._arrPolylines[lineNr].circleCoords[circleNr]); + var newLineSegment = this._generateLineCoords (this._arrPolylines[lineNr].circleCoords[circleNr-1], this._arrPolylines[lineNr].circleCoords[circleNr]); Array.prototype.splice.apply (lineCoords, [(circleNr-1)*(arcpoints-1), (2*arcpoints-1)].concat (newLineSegment)); this._arrPolylines[lineNr].arrowMarkers [circleNr-1].removeFrom (this._layerPaint); this._arrPolylines[lineNr].arrowMarkers [circleNr].removeFrom (this._layerPaint); var arrowMarker = this._drawArrow (newLineSegment); - this._arrPolylines[lineNr].arrowMarkers.splice(circleNr-1,2,arrowMarker); + this._arrPolylines[lineNr].arrowMarkers.splice(circleNr - 1, 2, arrowMarker); } this._arrPolylines[lineNr].polylinePath.setLatLngs (lineCoords); - this._arrPolylines[lineNr].arrowMarkers.map (function (item, index) { + this._arrPolylines[lineNr].arrowMarkers.map(function (item, index) { item.cntLine = lineNr; item.cntArrow = index; }); diff --git a/README.md b/README.md index 368bd39..356af86 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ options = { useSubunits: true, // Use subunits (metres/feet) in tooltips if distances are less than 1 kilometre/landmile clearMeasurementsOnStop: true, // Clear all measurements when Measure Control is switched off showBearings: false, // Whether bearings are displayed within the tooltips + useRhumbLines: false, // Whether to use simple rhumb lines instead of great circle measurements + arcpoints: 99, // Number of points to draw in each arc (when using default great circle method) bearingTextIn: 'In', // language dependend label for inbound bearings bearingTextOut: 'Out', // language dependend label for outbound bearings tooltipTextFinish: 'Click to finish line
', diff --git a/demo6.html b/demo6.html new file mode 100644 index 0000000..47b605e --- /dev/null +++ b/demo6.html @@ -0,0 +1,41 @@ + + + + + + Demo 6 of Leaflet.PolylineMeasure + + + + + + + + +
+ + + From 42c50014aa37e1e1e1ce351aaf50c477471a0c70 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Fri, 12 Jul 2024 15:44:32 +0100 Subject: [PATCH 2/7] Link to demo6.html (will be broken until pull request merged in) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 356af86..8990d23 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - [**Demo 3**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo3.html) (nautical mile units, bearings, without Unit Control and Clear Control buttons) - [**Demo 4**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo4.html) (two maps) - [**Demo 5**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo5.html) (programatically providing polyline points - "Seeding Data") +- [**Demo 6**](https://ppete2.github.io/Leaflet.PolylineMeasure/demo6.html) (using simple rhumb lines instead of great circles) ![Screenshot](https://ppete2.github.io/Leaflet.PolylineMeasure/screenshot.jpg) From 8f49ffedf9f197280d8a9c8a67f7694005a3dce1 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Mon, 15 Jul 2024 10:27:00 +0100 Subject: [PATCH 3/7] Add note about rhumb lines --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8990d23..cb6de83 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ * Measuring in **metric system** (metres, kilometres), in **imperial system** (feet, landmiles), or in **nautical miles**. * 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. * **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. +* Lines can be displayed as straight rhumb lines instead of great circle arcs if preferred. * To **finish** drawing a line just *doubleclick*, or *singleclick* onto the last (=orange) point, or *press "ESC"-key*. * **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)* * To **continue** a line after it has been finished, hold the *Ctrl-Key* while clicking onto the first or last point of a line. From e5374daa73a4212f33ae846344744cd43c7d5ce2 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Mon, 15 Jul 2024 10:28:36 +0100 Subject: [PATCH 4/7] Add bold for consistency --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb6de83..01239ee 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ * Measuring in **metric system** (metres, kilometres), in **imperial system** (feet, landmiles), or in **nautical miles**. * 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. * **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. -* Lines can be displayed as straight rhumb lines instead of great circle arcs if preferred. +* Lines can be displayed as straight **rhumb lines** instead of great-circle arcs if preferred. * To **finish** drawing a line just *doubleclick*, or *singleclick* onto the last (=orange) point, or *press "ESC"-key*. * **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)* * To **continue** a line after it has been finished, hold the *Ctrl-Key* while clicking onto the first or last point of a line. From 84a3ce2b2ab9935e37239b5a2d2076265b082462 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Mon, 15 Jul 2024 10:37:40 +0100 Subject: [PATCH 5/7] Remove debug code etc --- Leaflet.PolylineMeasure.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/Leaflet.PolylineMeasure.js b/Leaflet.PolylineMeasure.js index b0d942e..de76f96 100644 --- a/Leaflet.PolylineMeasure.js +++ b/Leaflet.PolylineMeasure.js @@ -777,7 +777,6 @@ /** * Update the tooltip distance - * @TODO param details * @param currentTooltip Current tooltip * @param prevTooltip Previous tooltip * @param {Number} total Total distance @@ -1163,8 +1162,6 @@ // get Coords of each Point of the current Polyline var lineCoords = this._arrPolylines[lineNr].polylinePath.getLatLngs(); - // TODO: GC uses 0/1 whereas rhumbLines use lat/lng for some reason ... debug/standardise if necessary - //console.log('lineCoords 1', lineCoords); // generate coords up to this point var arc1 = this._generateLineCoords (this._arrPolylines[lineNr].circleCoords[arrowNr], e.latlng); From 2c890c22d7f17af5db4367ae70f82ec3b012f609 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Mon, 15 Jul 2024 15:18:58 +0100 Subject: [PATCH 6/7] Add rhumb line distance calculation from the Movable Type Geodesy Library, using method described on http://www.movable-type.co.uk/scripts/latlong.html --- Leaflet.PolylineMeasure.js | 67 +++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/Leaflet.PolylineMeasure.js b/Leaflet.PolylineMeasure.js index de76f96..41ca647 100644 --- a/Leaflet.PolylineMeasure.js +++ b/Leaflet.PolylineMeasure.js @@ -568,11 +568,56 @@ this._arrPolylines.map (this._computeDistance.bind(this)); }, + /** + * Calculate the distance between two points + * + * @param {L.LatLng} _from Point to measure from + * @param {L.LatLng} _to Point to measure to + * @returns {number|*} + * @private + */ + _distanceBetween: function(_from, _to) { + return this.options.useRhumbLines ? this._rhumbDistance(_from, _to) : _from.distanceTo(_to); + }, + + /** + * Returns the distance travelling between two points along a rhumb line. + * + * see rhumbDistanceTo() from https://cdn.jsdelivr.net/npm/geodesy@2/latlon-spherical.js + * and www.edwilliams.org/avform.htm#Rhumb + * + * @param {L.LatLng} _from Point to measure from + * @param {L.LatLng} _to Point to measure to + * @param {number} radius Radius of earth + * @returns {number} Distance in km between this point and destination point (same units as radius). + * @private + */ + _rhumbDistance: function(_from, _to, radius=6371e3) { + const R = radius; + const φ1 = _from.lat * Math.PI / 180; + const φ2 = _to.lat * Math.PI / 180; + const Δφ = φ2 - φ1; + let Δλ = Math.abs(_to.lng - _from.lng) * Math.PI / 180; + // if dLon over 180° take shorter rhumb line across the anti-meridian: + if (Math.abs(Δλ) > Math.PI) Δλ = Δλ > 0 ? -(2 * Math.PI - Δλ) : (2 * Math.PI + Δλ); + + // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor' + // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it (note ε is too small) + const Δψ = Math.log(Math.tan(φ2 / 2 + Math.PI / 4) / Math.tan(φ1 / 2 + Math.PI / 4)); + const q = Math.abs(Δψ) > 10e-12 ? Δφ / Δψ : Math.cos(φ1); + + // distance is pythagoras on 'stretched' Mercator projection, √(Δφ² + q²·Δλ²) + const δ = Math.sqrt(Δφ*Δφ + q*q * Δλ*Δλ); // angular distance in radians + const d = δ * R; + + return d; + }, + _computeDistance: function(line) { var totalDistance = 0; line.circleCoords.map (function(point, point_index) { if (point_index >= 1) { - var distance = line.circleCoords [point_index - 1].distanceTo (line.circleCoords [point_index]); + var distance = this._distanceBetween(line.circleCoords [point_index - 1], line.circleCoords [point_index]); totalDistance += distance; this._updateTooltip (line.tooltips [point_index], line.tooltips [point_index - 1], totalDistance, distance, line.circleCoords [point_index - 1], line.circleCoords [point_index]); } @@ -606,7 +651,7 @@ this._arrPolylines[lineNr].tooltips [0]._icon.innerHTML = text; this._arrPolylines[lineNr].tooltips.map (function (item, index) { if (index >= 1) { - var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); + var distance = this._distanceBetween(this._arrPolylines[lineNr].circleCoords[index-1], this._arrPolylines[lineNr].circleCoords[index]); var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; totalDistance += distance; @@ -919,7 +964,7 @@ var currentTooltip = this._currentLine.tooltips.last(); var prevTooltip = this._currentLine.tooltips.slice(-2,-1)[0]; currentTooltip.setLatLng (mouseCoords); - var distanceSegment = mouseCoords.distanceTo (lastCircleCoords); + var distanceSegment = this._distanceBetween(mouseCoords, lastCircleCoords); this._updateTooltip (currentTooltip, prevTooltip, this._currentLine.distance + distanceSegment, distanceSegment, lastCircleCoords, mouseCoords); }, @@ -1003,7 +1048,7 @@ arrowMarker.cntLine = polylineState._currentLine.id; arrowMarker.cntArrow = polylineState._cntCircle - 1; polylineState._currentLine.arrowMarkers.push (arrowMarker); - var distanceSegment = lastCircleCoords.distanceTo (mouseCoords); + var distanceSegment = polylineState._distanceBetween(lastCircleCoords, mouseCoords); this.distance += distanceSegment; var currentTooltip = polylineState._currentLine.tooltips.last(); var prevTooltip = polylineState._currentLine.tooltips.slice(-1,-2)[0]; @@ -1202,7 +1247,7 @@ var totalDistance = 0; this._arrPolylines[lineNr].tooltips.map (function (item, index) { if (index >= 1) { - var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); + var distance = this._distanceBetween(this._arrPolylines[lineNr].circleCoords[index-1], this._arrPolylines[lineNr].circleCoords[index]); var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; totalDistance += distance; @@ -1273,7 +1318,7 @@ // update tooltip texts of each tooltip this._arrPolylines[lineNr].tooltips.map (function (item, index) { if (index >= 1) { - var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); + var distance = this._distanceBetween(this._arrPolylines[lineNr].circleCoords[index-1], this._arrPolylines[lineNr].circleCoords[index]); var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; totalDistance += distance; @@ -1292,7 +1337,7 @@ this._rubberlinePath2.setLatLngs (this._generateLineCoords (mouseCoords, currentCircleCoords)); this._tooltipNew.setLatLng (mouseCoords); var totalDistance = 0; - var distance = mouseCoords.distanceTo (this._arrPolylines[lineNr].circleCoords[0]); + var distance = this._distanceBetween(mouseCoords, this._arrPolylines[lineNr].circleCoords[0]); var lastCircleCoords = mouseCoords; var currentCoords = this._arrPolylines[lineNr].circleCoords[0]; totalDistance += distance; @@ -1301,7 +1346,7 @@ this._updateTooltip (currentTooltip, prevTooltip, totalDistance, distance, lastCircleCoords, currentCoords); this._arrPolylines[lineNr].tooltips.map (function (item, index) { if (index >= 1) { - var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); + var distance = this._distanceBetween(this._arrPolylines[lineNr].circleCoords[index-1], this._arrPolylines[lineNr].circleCoords[index]); var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; totalDistance += distance; @@ -1459,12 +1504,12 @@ var prevTooltip = this._currentLine.tooltips[index-1]; var lastCircleCoords = this._currentLine.circleCoords[index - 1]; if(index === arr.length - 1) { - distance = this._currentLine.circleCoords[index-1].distanceTo (e1.latlng); + distance = this._distanceBetween(this._currentLine.circleCoords[index-1], e1.latlng); mouseCoords = e1.latlng; // if this is the last Circle (mouse cursor) then don't sum the distance, but update tooltip like it was summed this._updateTooltip (item, prevTooltip, totalDistanceUnfinishedLine + distance, distance, lastCircleCoords, mouseCoords); } else { - distance = this._currentLine.circleCoords[index-1].distanceTo (this._currentLine.circleCoords[index]); + distance = this._distanceBetween(this._currentLine.circleCoords[index-1], this._currentLine.circleCoords[index]); mouseCoords = this._currentLine.circleCoords[index]; // if this is not the last Circle (mouse cursor) then sum the distance totalDistanceUnfinishedLine += distance; @@ -1553,7 +1598,7 @@ var totalDistance = 0; this._arrPolylines[lineNr].tooltips.map (function (item, index) { if (index >= 1) { - var distance = this._arrPolylines[lineNr].circleCoords[index-1].distanceTo (this._arrPolylines[lineNr].circleCoords[index]); + var distance = this._distanceBetween(this._arrPolylines[lineNr].circleCoords[index-1], this._arrPolylines[lineNr].circleCoords[index]); var lastCircleCoords = this._arrPolylines[lineNr].circleCoords[index - 1]; var mouseCoords = this._arrPolylines[lineNr].circleCoords[index]; totalDistance += distance; From d90c95f12988a997bfbe566c27adfff898ec8034 Mon Sep 17 00:00:00 2001 From: Dan Karran Date: Tue, 16 Jul 2024 13:46:37 +0100 Subject: [PATCH 7/7] =?UTF-8?q?Don't=20take=20a=20shorter=20route=20over?= =?UTF-8?q?=20180=C2=BA,=20take=20the=20prescribed=20route?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Leaflet.PolylineMeasure.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Leaflet.PolylineMeasure.js b/Leaflet.PolylineMeasure.js index 41ca647..fa20c84 100644 --- a/Leaflet.PolylineMeasure.js +++ b/Leaflet.PolylineMeasure.js @@ -598,8 +598,6 @@ const φ2 = _to.lat * Math.PI / 180; const Δφ = φ2 - φ1; let Δλ = Math.abs(_to.lng - _from.lng) * Math.PI / 180; - // if dLon over 180° take shorter rhumb line across the anti-meridian: - if (Math.abs(Δλ) > Math.PI) Δλ = Δλ > 0 ? -(2 * Math.PI - Δλ) : (2 * Math.PI + Δλ); // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor' // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it (note ε is too small) @@ -860,9 +858,6 @@ var y = Math.log(Math.tan(Math.PI/4+lat2/2)/Math.tan(Math.PI/4+lat1/2)); var x = lng2 - lng1; - // if dLon over 180° take shorter rhumb line across the anti-meridian: - if (Math.abs(x) > Math.PI) x = x>0 ? -(2*Math.PI-x) : (2*Math.PI+x); - if (direction === "inbound") { var brng = Math.atan2(x, y) * 180/Math.PI - 180; } else {