leaflet.geometryutil.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. var L = L || exports;
  2. (function () {
  3. "use strict";
  4. /**
  5. * @fileOverview Leaflet Geometry utilities for distances and linear referencing.
  6. * @name L.GeometryUtil
  7. */
  8. L.GeometryUtil = {
  9. /**
  10. Shortcut function for planar distance between two {L.LatLng} at current zoom.
  11. @param {L.Map} map
  12. @param {L.LatLng} latlngA
  13. @param {L.LatLng} latlngB
  14. @returns {Number} in pixels
  15. */
  16. distance: function (map, latlngA, latlngB) {
  17. return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
  18. },
  19. /**
  20. Shortcut function for planar distance between a {L.LatLng} and a segment (A-B).
  21. @param {L.Map} map
  22. @param {L.LatLng} latlng
  23. @param {L.LatLng} latlngA
  24. @param {L.LatLng} latlngB
  25. @returns {Number} in pixels
  26. */
  27. distanceSegment: function (map, latlng, latlngA, latlngB) {
  28. var p = map.latLngToLayerPoint(latlng),
  29. p1 = map.latLngToLayerPoint(latlngA),
  30. p2 = map.latLngToLayerPoint(latlngB);
  31. return L.LineUtil.pointToSegmentDistance(p, p1, p2);
  32. },
  33. /**
  34. Returns true if the latlng belongs to segment.
  35. param {L.LatLng} latlng
  36. @param {L.LatLng} latlngA
  37. @param {L.LatLng} latlngB
  38. @param {?Number} [tolerance=0.2]
  39. @returns {boolean}
  40. */
  41. belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
  42. tolerance = tolerance === undefined ? 0.2 : tolerance;
  43. var hypotenuse = latlngA.distanceTo(latlngB),
  44. delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
  45. return delta/hypotenuse < tolerance;
  46. },
  47. /**
  48. * Returns total length of line
  49. * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>}
  50. * @returns {Number} in meters
  51. */
  52. length: function (coords) {
  53. var accumulated = L.GeometryUtil.accumulatedLengths(coords);
  54. return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
  55. },
  56. /**
  57. * Returns a list of accumulated length along a line.
  58. * @param {L.Polyline|Array<L.Point>|Array<L.LatLng>}
  59. * @returns {Number} in meters
  60. */
  61. accumulatedLengths: function (coords) {
  62. if (typeof coords.getLatLngs == 'function') {
  63. coords = coords.getLatLngs();
  64. }
  65. if (coords.length === 0)
  66. return [];
  67. var total = 0,
  68. lengths = [0];
  69. for (var i = 0, n = coords.length - 1; i< n; i++) {
  70. total += coords[i].distanceTo(coords[i+1]);
  71. lengths.push(total);
  72. }
  73. return lengths;
  74. },
  75. /**
  76. Returns the closest point of a {L.LatLng} on the segment (A-B)
  77. @param {L.Map} map
  78. @param {L.LatLng} latlng
  79. @param {L.LatLng} latlngA
  80. @param {L.LatLng} latlngB
  81. @returns {L.LatLng}
  82. */
  83. closestOnSegment: function (map, latlng, latlngA, latlngB) {
  84. var maxzoom = map.getMaxZoom();
  85. if (maxzoom === Infinity)
  86. maxzoom = map.getZoom();
  87. var p = map.project(latlng, maxzoom),
  88. p1 = map.project(latlngA, maxzoom),
  89. p2 = map.project(latlngB, maxzoom),
  90. closest = L.LineUtil.closestPointOnSegment(p, p1, p2);
  91. return map.unproject(closest, maxzoom);
  92. },
  93. /**
  94. Returns the closest latlng on layer.
  95. @param {L.Map} map
  96. @param {Array<L.LatLng>|L.PolyLine} layer - Layer that contains the result.
  97. @param {L.LatLng} latlng
  98. @param {?boolean} [vertices=false] - Whether to restrict to path vertices.
  99. @returns {L.LatLng}
  100. */
  101. closest: function (map, layer, latlng, vertices) {
  102. if (typeof layer.getLatLngs != 'function')
  103. layer = L.polyline(layer);
  104. var latlngs = layer.getLatLngs(),
  105. mindist = Infinity,
  106. result = null,
  107. i, n, distance;
  108. // Lookup vertices
  109. if (vertices) {
  110. for(i = 0, n = latlngs.length; i < n; i++) {
  111. var ll = latlngs[i];
  112. distance = L.GeometryUtil.distance(map, latlng, ll);
  113. if (distance < mindist) {
  114. mindist = distance;
  115. result = ll;
  116. result.distance = distance;
  117. }
  118. }
  119. return result;
  120. }
  121. if (layer instanceof L.Polygon) {
  122. latlngs.push(latlngs[0]);
  123. }
  124. // Keep the closest point of all segments
  125. for (i = 0, n = latlngs.length; i < n-1; i++) {
  126. var latlngA = latlngs[i],
  127. latlngB = latlngs[i+1];
  128. distance = L.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
  129. if (distance <= mindist) {
  130. mindist = distance;
  131. result = L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
  132. result.distance = distance;
  133. }
  134. }
  135. return result;
  136. },
  137. /**
  138. Returns the closest layer to latlng among a list of layers.
  139. @param {L.Map} map
  140. @param {Array<L.ILayer>} layers
  141. @param {L.LatLng} latlng
  142. @returns {object} with layer, latlng and distance or {null} if list is empty;
  143. */
  144. closestLayer: function (map, layers, latlng) {
  145. var mindist = Infinity,
  146. result = null,
  147. ll = null,
  148. distance = Infinity;
  149. for (var i = 0, n = layers.length; i < n; i++) {
  150. var layer = layers[i];
  151. // Single dimension, snap on points, else snap on closest
  152. if (typeof layer.getLatLng == 'function') {
  153. ll = layer.getLatLng();
  154. distance = L.GeometryUtil.distance(map, latlng, ll);
  155. }
  156. else {
  157. ll = L.GeometryUtil.closest(map, layer, latlng);
  158. if (ll) distance = ll.distance; // Can return null if layer has no points.
  159. }
  160. if (distance < mindist) {
  161. mindist = distance;
  162. result = {layer: layer, latlng: ll, distance: distance};
  163. }
  164. }
  165. return result;
  166. },
  167. /**
  168. Returns the closest position from specified {LatLng} among specified layers,
  169. with a maximum tolerance in pixels, providing snapping behaviour.
  170. @param {L.Map} map
  171. @param {Array<ILayer>} layers - A list of layers to snap on.
  172. @param {L.LatLng} latlng - The position to snap.
  173. @param {?Number} [tolerance=Infinity] - Maximum number of pixels.
  174. @param {?boolean} [withVertices=true] - Snap to layers vertices.
  175. @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
  176. */
  177. closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
  178. tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
  179. withVertices = typeof withVertices == 'boolean' ? withVertices : true;
  180. var result = L.GeometryUtil.closestLayer(map, layers, latlng);
  181. if (!result || result.distance > tolerance)
  182. return null;
  183. // If snapped layer is linear, try to snap on vertices (extremities and middle points)
  184. if (withVertices && typeof result.layer.getLatLngs == 'function') {
  185. var closest = L.GeometryUtil.closest(map, result.layer, result.latlng, true);
  186. if (closest.distance < tolerance) {
  187. result.latlng = closest;
  188. result.distance = L.GeometryUtil.distance(map, closest, latlng);
  189. }
  190. }
  191. return result;
  192. },
  193. /**
  194. Returns the Point located on a segment at the specified ratio of the segment length.
  195. @param {L.Point} pA
  196. @param {L.Point} pB
  197. @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
  198. @returns {L.Point} the interpolated point.
  199. */
  200. interpolateOnPointSegment: function (pA, pB, ratio) {
  201. return L.point(
  202. (pA.x * (1 - ratio)) + (ratio * pB.x),
  203. (pA.y * (1 - ratio)) + (ratio * pB.y)
  204. );
  205. },
  206. /**
  207. Returns the coordinate of the point located on a line at the specified ratio of the line length.
  208. @param {L.Map} map
  209. @param {Array<L.LatLng>|L.PolyLine} latlngs
  210. @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive
  211. @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
  212. (-1 if the interpolated point is the first vertex)
  213. */
  214. interpolateOnLine: function (map, latLngs, ratio) {
  215. latLngs = (latLngs instanceof L.Polyline) ? latLngs.getLatLngs() : latLngs;
  216. var n = latLngs.length;
  217. if (n < 2) {
  218. return null;
  219. }
  220. if (ratio === 0) {
  221. return {latLng: latLngs[0],
  222. predecessor: -1};
  223. }
  224. if (ratio == 1) {
  225. return {latLng: latLngs[latLngs.length -1],
  226. predecessor: latLngs.length-2};
  227. }
  228. // ensure the ratio is between 0 and 1;
  229. ratio = Math.max(Math.min(ratio, 1), 0);
  230. // project the LatLngs as Points,
  231. // and compute total planar length of the line at max precision
  232. var maxzoom = map.getMaxZoom();
  233. if (maxzoom === Infinity)
  234. maxzoom = map.getZoom();
  235. var pts = [];
  236. var lineLength = 0;
  237. for(var i = 0; i < n; i++) {
  238. pts[i] = map.project(latLngs[i], maxzoom);
  239. if(i > 0)
  240. lineLength += pts[i-1].distanceTo(pts[i]);
  241. }
  242. var ratioDist = lineLength * ratio;
  243. var a = pts[0],
  244. b = pts[1],
  245. distA = 0,
  246. distB = a.distanceTo(b);
  247. // follow the line segments [ab], adding lengths,
  248. // until we find the segment where the points should lie on
  249. var index = 1;
  250. for (; index < n && distB < ratioDist; index++) {
  251. a = b;
  252. distA = distB;
  253. b = pts[index];
  254. distB += a.distanceTo(b);
  255. }
  256. // compute the ratio relative to the segment [ab]
  257. var segmentRatio = ((distB - distA) !== 0) ? ((ratioDist - distA) / (distB - distA)) : 0;
  258. var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment(a, b, segmentRatio);
  259. return {
  260. latLng: map.unproject(interpolatedPoint, maxzoom),
  261. predecessor: index-2
  262. };
  263. },
  264. /**
  265. Returns a float between 0 and 1 representing the location of the
  266. closest point on polyline to the given latlng, as a fraction of total 2d line length.
  267. (opposite of L.GeometryUtil.interpolateOnLine())
  268. @param {L.Map} map
  269. @param {L.PolyLine} polyline
  270. @param {L.LatLng} latlng
  271. @returns {Number}
  272. */
  273. locateOnLine: function (map, polyline, latlng) {
  274. var latlngs = polyline.getLatLngs();
  275. if (latlng.equals(latlngs[0]))
  276. return 0.0;
  277. if (latlng.equals(latlngs[latlngs.length-1]))
  278. return 1.0;
  279. var point = L.GeometryUtil.closest(map, polyline, latlng, false),
  280. lengths = L.GeometryUtil.accumulatedLengths(latlngs),
  281. total_length = lengths[lengths.length-1],
  282. portion = 0,
  283. found = false;
  284. for (var i=0, n = latlngs.length-1; i < n; i++) {
  285. var l1 = latlngs[i],
  286. l2 = latlngs[i+1];
  287. portion = lengths[i];
  288. if (L.GeometryUtil.belongsSegment(point, l1, l2)) {
  289. portion += l1.distanceTo(point);
  290. found = true;
  291. break;
  292. }
  293. }
  294. if (!found) {
  295. throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
  296. }
  297. return portion / total_length;
  298. },
  299. /**
  300. Returns a clone with reversed coordinates.
  301. @param {L.PolyLine} polyline
  302. @returns {L.PolyLine}
  303. */
  304. reverse: function (polyline) {
  305. return L.polyline(polyline.getLatLngs().slice(0).reverse());
  306. },
  307. /**
  308. Returns a sub-part of the polyline, from start to end.
  309. If start is superior to end, returns extraction from inverted line.
  310. @param {L.Map} map
  311. @param {L.PolyLine} latlngs
  312. @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
  313. @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
  314. @returns {Array<L.LatLng>}
  315. */
  316. extract: function (map, polyline, start, end) {
  317. if (start > end) {
  318. return L.GeometryUtil.extract(map, L.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
  319. }
  320. // Bound start and end to [0-1]
  321. start = Math.max(Math.min(start, 1), 0);
  322. end = Math.max(Math.min(end, 1), 0);
  323. var latlngs = polyline.getLatLngs(),
  324. startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start),
  325. endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end);
  326. // Return single point if start == end
  327. if (start == end) {
  328. var point = L.GeometryUtil.interpolateOnLine(map, polyline, end);
  329. return [point.latLng];
  330. }
  331. // Array.slice() works indexes at 0
  332. if (startpoint.predecessor == -1)
  333. startpoint.predecessor = 0;
  334. if (endpoint.predecessor == -1)
  335. endpoint.predecessor = 0;
  336. var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
  337. result.unshift(startpoint.latLng);
  338. result.push(endpoint.latLng);
  339. return result;
  340. },
  341. /**
  342. Returns true if first polyline ends where other second starts.
  343. @param {L.PolyLine} polyline
  344. @param {L.PolyLine} other
  345. @returns {bool}
  346. */
  347. isBefore: function (polyline, other) {
  348. if (!other) return false;
  349. var lla = polyline.getLatLngs(),
  350. llb = other.getLatLngs();
  351. return (lla[lla.length-1]).equals(llb[0]);
  352. },
  353. /**
  354. Returns true if first polyline starts where second ends.
  355. @param {L.PolyLine} polyline
  356. @param {L.PolyLine} other
  357. @returns {bool}
  358. */
  359. isAfter: function (polyline, other) {
  360. if (!other) return false;
  361. var lla = polyline.getLatLngs(),
  362. llb = other.getLatLngs();
  363. return (lla[0]).equals(llb[llb.length-1]);
  364. },
  365. /**
  366. Returns true if first polyline starts where second ends or start.
  367. @param {L.PolyLine} polyline
  368. @param {L.PolyLine} other
  369. @returns {bool}
  370. */
  371. startsAtExtremity: function (polyline, other) {
  372. if (!other) return false;
  373. var lla = polyline.getLatLngs(),
  374. llb = other.getLatLngs(),
  375. start = lla[0];
  376. return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
  377. },
  378. /**
  379. Returns horizontal angle in degres between two points.
  380. @param {L.Point} a
  381. @param {L.Point} b
  382. @returns {float}
  383. */
  384. computeAngle: function(a, b) {
  385. return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
  386. },
  387. /**
  388. Returns slope (Ax+B) between two points.
  389. @param {L.Point} a
  390. @param {L.Point} b
  391. @returns {Object} with ``a`` and ``b`` properties.
  392. */
  393. computeSlope: function(a, b) {
  394. var s = (b.y - a.y) / (b.x - a.x),
  395. o = a.y - (s * a.x);
  396. return {'a': s, 'b': o};
  397. },
  398. /**
  399. Returns LatLng of rotated point around specified LatLng center.
  400. @param {L.LatLng} latlngPoint: point to rotate
  401. @param {double} angleDeg: angle to rotate in degrees
  402. @param {L.LatLng} latlngCenter: center of rotation
  403. @returns {L.LatLng} rotated point
  404. */
  405. rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
  406. var maxzoom = map.getMaxZoom();
  407. if (maxzoom === Infinity)
  408. maxzoom = map.getZoom();
  409. var angleRad = angleDeg*Math.PI/180,
  410. pPoint = map.project(latlngPoint, maxzoom),
  411. pCenter = map.project(latlngCenter, maxzoom),
  412. x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
  413. y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
  414. return map.unproject(new L.Point(x2,y2), maxzoom);
  415. }
  416. };
  417. }());