index.js (8429B)
1 import L from 'leaflet'; 2 import './measured_line.css'; 3 4 function pointOnSegmentAtDistance(p1, p2, dist) { 5 // FIXME: we should place markers along projected line to avoid transformation distortions 6 var q = dist / p1.distanceTo(p2), 7 x = p1.lng + (p2.lng - p1.lng) * q, 8 y = p1.lat + (p2.lat - p1.lat) * q; 9 return L.latLng(y, x); 10 } 11 12 function sinCosFromLatLonSegment(segment) { 13 const 14 p1 = L.CRS.EPSG3857.project(segment[0]), 15 p2 = L.CRS.EPSG3857.project(segment[1]), 16 dx = p2.x - p1.x, 17 dy = p1.y - p2.y, 18 len = Math.sqrt(dx * dx + dy * dy), 19 sin = dy / len, 20 cos = dx / len; 21 return [sin, cos]; 22 } 23 24 L.MeasuredLine = L.Polyline.extend({ 25 options: { 26 minTicksIntervalMm: 15, 27 }, 28 29 onAdd: function(map) { 30 L.Polyline.prototype.onAdd.call(this, map); 31 this._ticks = {}; 32 this.updateTicks(); 33 this._map.on('zoomend', this.updateTicks, this); 34 // markers are created only for visible part of map, need to update when it changes 35 this._map.on('moveend', this.updateTicks, this); 36 this.on('nodeschanged', this.updateTicksLater, this); 37 }, 38 39 updateTicksLater: function() { 40 setTimeout(this.updateTicks.bind(this), 0); 41 }, 42 43 onRemove: function(map) { 44 this._map.off('zoomend', this.updateTicks, this); 45 this._map.off('moveend', this.updateTicks, this); 46 this.off('nodeschanged', this.updateTicks, this); 47 this._clearTicks(); 48 L.Polyline.prototype.onRemove.call(this, map); 49 }, 50 51 _clearTicks: function() { 52 if (this._map) { 53 Object.values(this._ticks).forEach((tick) => this._map.removeLayer(tick)); 54 this._ticks = {}; 55 } 56 }, 57 58 _addTick: function(tick, marker) { 59 var transformMatrixString = 'matrix(' + tick.transformMatrix.join(',') + ')'; 60 if (marker) { 61 marker._icon.childNodes[0].style.transform = transformMatrixString; 62 marker.setLatLng(tick.position); 63 } else { 64 var labelText = Math.round((tick.distanceValue / 10)) / 100 + ' km', 65 icon = L.divIcon( 66 { 67 html: '<div class="measure-tick-icon-text" style="transform:' + 68 transformMatrixString + '">' + 69 labelText + '</div>', 70 className: 'measure-tick-icon' 71 } 72 ); 73 marker = L.marker(tick.position, { 74 icon: icon, 75 interactive: false, 76 keyboard: false, 77 projectedShift: () => this.shiftProjectedFitMapView(), 78 }); 79 marker.addTo(this._map); 80 } 81 this._ticks[tick.distanceValue.toString()] = marker; 82 }, 83 84 setMeasureTicksVisible: function(visible) { 85 this.options.measureTicksShown = visible; 86 this.updateTicks(); 87 }, 88 89 getTicksPositions: function(minTicksIntervalMeters, bounds) { 90 var steps = [500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, 1000000]; 91 var ticks = []; 92 93 const that = this; 94 function addTick(position, segment, distanceValue) { 95 if (bounds) { 96 // create markers only in visible part of map 97 const normalizedBounds = that._map.wrapLatLngBounds(bounds); 98 const normalizedPosition = position.wrap(); 99 // account for worldCopyJump 100 const positionMinus360 = L.latLng(normalizedPosition.lat, normalizedPosition.lng - 360); 101 const positionPlus360 = L.latLng(normalizedPosition.lat, normalizedPosition.lng + 360); 102 if ( 103 !normalizedBounds.contains(normalizedPosition) && 104 !normalizedBounds.contains(positionMinus360) && 105 !normalizedBounds.contains(positionPlus360) 106 ) { 107 return; 108 } 109 } 110 111 var sinCos = sinCosFromLatLonSegment(segment), 112 sin = sinCos[0], 113 cos = sinCos[1], 114 transformMatrix; 115 116 if (sin > 0) { 117 transformMatrix = [sin, -cos, cos, sin, 0, 0]; 118 } else { 119 transformMatrix = [-sin, cos, -cos, -sin, 0, 0]; 120 } 121 ticks.push({position: position, distanceValue: distanceValue, transformMatrix: transformMatrix}); 122 } 123 124 let step; 125 for (step of steps) { 126 if (step >= minTicksIntervalMeters) { 127 break; 128 } 129 } 130 131 var lastTickMeasure = 0, 132 lastPointMeasure = 0, 133 points = this._latlngs, 134 points_n = points.length, 135 nextPointMeasure, 136 segmentLength; 137 if (points_n < 2) { 138 return ticks; 139 } 140 141 for (var i = 1; i < points_n; i++) { 142 segmentLength = points[i].distanceTo(points[i - 1]); 143 nextPointMeasure = lastPointMeasure + segmentLength; 144 if (nextPointMeasure >= lastTickMeasure + step) { 145 while (lastTickMeasure + step <= nextPointMeasure) { 146 lastTickMeasure += step; 147 addTick( 148 pointOnSegmentAtDistance(points[i - 1], points[i], lastTickMeasure - lastPointMeasure), 149 [points[i - 1], points[i]], 150 lastTickMeasure 151 ); 152 } 153 } 154 lastPointMeasure = nextPointMeasure; 155 } 156 // remove last mark if it is close to track end 157 if (lastPointMeasure - lastTickMeasure < minTicksIntervalMeters / 2) { 158 ticks.pop(); 159 } 160 // special case: if track is versy short, do not add starting mark 161 if (lastPointMeasure > minTicksIntervalMeters / 2) { 162 addTick(points[0], [points[0], points[1]], 0); 163 } 164 addTick(points[points_n - 1], [points[points_n - 2], points[points_n - 1]], lastPointMeasure); 165 return ticks; 166 }, 167 168 updateTicks: function() { 169 if (!this._map) { 170 return; 171 } 172 if (!this.options.measureTicksShown) { 173 this._clearTicks(); 174 return; 175 } 176 var bounds = this._map.getBounds().pad(1), 177 rad = Math.PI / 180, 178 dpi = 96, 179 mercatorMetersPerPixel = 20003931 / (this._map.project([180, 0]).x), 180 referencePoint = this.getLatLngs().length ? this.getBounds().getCenter() : this._map.getCenter(), 181 realMetersPerPixel = mercatorMetersPerPixel * Math.cos(referencePoint.lat * rad), 182 mapScale = 1 / dpi * 2.54 / 100 / realMetersPerPixel, 183 minTicksIntervalMeters = this.options.minTicksIntervalMm / mapScale / 1000, 184 ticks = this.getTicksPositions(minTicksIntervalMeters, bounds), 185 oldTicks = this._ticks; 186 this._ticks = {}; 187 ticks.forEach(function(tick) { 188 var oldMarker = oldTicks[tick.distanceValue.toString()]; 189 this._addTick(tick, oldMarker); 190 if (oldMarker) { 191 delete oldTicks[tick.distanceValue.toString()]; 192 } 193 }.bind(this) 194 ); 195 Object.values(oldTicks).forEach((tick) => this._map.removeLayer(tick)); 196 }, 197 198 getLength: function() { 199 var points = this._latlngs, 200 points_n = points.length, 201 length = 0; 202 203 for (var i = 1; i < points_n; i++) { 204 length += points[i].distanceTo(points[i - 1]); 205 } 206 return length; 207 } 208 } 209 ); 210 211 L.measuredLine = function(latlngs, options) { 212 return new L.MeasuredLine(latlngs, options); 213 };