bm.geometryutil.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. // Packaging/modules magic dance.
  2. (function (factory) {
  3. factory(window.BM)
  4. }(function (BM) {
  5. "use strict";
  6. BM.Polyline._flat = BM.LineUtil.isFlat || BM.Polyline._flat || function (latlngs) {
  7. // true if it's a flat array of latlngs; false if nested
  8. return !BM.Util.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined');
  9. };
  10. /**
  11. * @fileOverview Geometry utilities for distances and linear referencing.
  12. * @name BM.GeometryUtil
  13. */
  14. BM.GeometryUtil = BM.extend(BM.GeometryUtil || {}, {
  15. /**
  16. Shortcut function for planar distance between two {BM.LatLng} at current zoom.
  17. @tutorial distance-length
  18. @param {BM.Map} map map to be used for this method
  19. @param {BM.LatLng} latlngA geographical point A
  20. @param {BM.LatLng} latlngB geographical point B
  21. @returns {Number} planar distance
  22. */
  23. distance: function (map, latlngA, latlngB) {
  24. return map.latLngToLayerPoint(latlngA).distanceTo(map.latLngToLayerPoint(latlngB));
  25. },
  26. /**
  27. Shortcut function for planar distance between a {BM.LatLng} and a segment (A-B).
  28. @param {BM.Map} map map to be used for this method
  29. @param {BM.LatLng} latlng - The position to search
  30. @param {BM.LatLng} latlngA geographical point A of the segment
  31. @param {BM.LatLng} latlngB geographical point B of the segment
  32. @returns {Number} planar distance
  33. */
  34. distanceSegment: function (map, latlng, latlngA, latlngB) {
  35. var p = map.latLngToLayerPoint(latlng),
  36. p1 = map.latLngToLayerPoint(latlngA),
  37. p2 = map.latLngToLayerPoint(latlngB);
  38. return BM.LineUtil.pointToSegmentDistance(p, p1, p2);
  39. },
  40. /**
  41. Shortcut function for converting distance to readable distance.
  42. @param {Number} distance distance to be converted
  43. @param {String} unit 'metric' or 'imperial'
  44. @returns {String} in yard or miles
  45. */
  46. readableDistance: function (distance, unit) {
  47. var isMetric = (unit !== 'imperial'),
  48. distanceStr;
  49. if (isMetric) {
  50. // show metres when distance is < 1km, then show km
  51. if (distance > 1000) {
  52. distanceStr = (distance / 1000).toFixed(2) + ' km';
  53. }
  54. else {
  55. distanceStr = Math.ceil(distance) + ' m';
  56. }
  57. }
  58. else {
  59. distance *= 1.09361;
  60. if (distance > 1760) {
  61. distanceStr = (distance / 1760).toFixed(2) + ' miles';
  62. }
  63. else {
  64. distanceStr = Math.ceil(distance) + ' yd';
  65. }
  66. }
  67. return distanceStr;
  68. },
  69. /**
  70. Returns true if the latlng belongs to segment A-B
  71. @param {BM.LatLng} latlng - The position to search
  72. @param {BM.LatLng} latlngA geographical point A of the segment
  73. @param {BM.LatLng} latlngB geographical point B of the segment
  74. @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really
  75. @returns {boolean}
  76. */
  77. belongsSegment: function(latlng, latlngA, latlngB, tolerance) {
  78. tolerance = tolerance === undefined ? 0.2 : tolerance;
  79. var hypotenuse = latlngA.distanceTo(latlngB),
  80. delta = latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse;
  81. return delta/hypotenuse < tolerance;
  82. },
  83. /**
  84. * Returns total length of line
  85. * @tutorial distance-length
  86. *
  87. * @param {BM.Polyline|Array<BM.Point>|Array<BM.LatLng>} coords Set of coordinates
  88. * @returns {Number} Total length (pixels for Point, meters for LatLng)
  89. */
  90. length: function (coords) {
  91. var accumulated = BM.GeometryUtil.accumulatedLengths(coords);
  92. return accumulated.length > 0 ? accumulated[accumulated.length-1] : 0;
  93. },
  94. /**
  95. * Returns a list of accumulated length along a line.
  96. * @param {BM.Polyline|Array<BM.Point>|Array<BM.LatLng>} coords Set of coordinates
  97. * @returns {Array<Number>} Array of accumulated lengths (pixels for Point, meters for LatLng)
  98. */
  99. accumulatedLengths: function (coords) {
  100. if (typeof coords.getLatLngs == 'function') {
  101. coords = coords.getLatLngs();
  102. }
  103. if (coords.length === 0)
  104. return [];
  105. var total = 0,
  106. lengths = [0];
  107. for (var i = 0, n = coords.length - 1; i< n; i++) {
  108. total += coords[i].distanceTo(coords[i+1]);
  109. lengths.push(total);
  110. }
  111. return lengths;
  112. },
  113. /**
  114. Returns the closest point of a {BM.LatLng} on the segment (A-B)
  115. @tutorial closest
  116. @param {BM.Map} map map to be used for this method
  117. @param {BM.LatLng} latlng - The position to search
  118. @param {BM.LatLng} latlngA geographical point A of the segment
  119. @param {BM.LatLng} latlngB geographical point B of the segment
  120. @returns {BM.LatLng} Closest geographical point
  121. */
  122. closestOnSegment: function (map, latlng, latlngA, latlngB) {
  123. var maxzoom = map.getMaxZoom();
  124. if (maxzoom === Infinity)
  125. maxzoom = map.getZoom();
  126. var p = map.project(latlng, maxzoom),
  127. p1 = map.project(latlngA, maxzoom),
  128. p2 = map.project(latlngB, maxzoom),
  129. closest = BM.LineUtil.closestPointOnSegment(p, p1, p2);
  130. return map.unproject(closest, maxzoom);
  131. },
  132. /**
  133. Returns the closest latlng on layer.
  134. Accept nested arrays
  135. @tutorial closest
  136. @param {BM.Map} map map to be used for this method
  137. @param {Array<BM.LatLng>|Array<Array<BM.LatLng>>|BM.PolyLine|BM.Polygon} layer - Layer that contains the result
  138. @param {BM.LatLng} latlng - The position to search
  139. @param {?boolean} [vertices=false] - Whether to restrict to path vertices.
  140. @returns {BM.LatLng} Closest geographical point or null if layer param is incorrect
  141. */
  142. closest: function (map, layer, latlng, vertices) {
  143. var latlngs,
  144. mindist = Infinity,
  145. result = null,
  146. i, n, distance, subResult;
  147. if (layer instanceof Array) {
  148. // if layer is Array<Array<T>>
  149. if (layer[0] instanceof Array && typeof layer[0][0] !== 'number') {
  150. // if we have nested arrays, we calc the closest for each array
  151. // recursive
  152. for (i = 0; i < layer.length; i++) {
  153. subResult = BM.GeometryUtil.closest(map, layer[i], latlng, vertices);
  154. if (subResult.distance < mindist) {
  155. mindist = subResult.distance;
  156. result = subResult;
  157. }
  158. }
  159. return result;
  160. } else if (layer[0] instanceof BM.LatLng
  161. || typeof layer[0][0] === 'number'
  162. || typeof layer[0].lat === 'number') { // we could have a latlng as [x,y] with x & y numbers or {lat, lng}
  163. layer = BM.polyline(layer);
  164. } else {
  165. return result;
  166. }
  167. }
  168. // if we don't have here a Polyline, that means layer is incorrect
  169. // see https://github.com/makinacorpus/.GeometryUtil/issues/23
  170. if (! ( layer instanceof BM.Polyline ) )
  171. return result;
  172. // deep copy of latlngs
  173. latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0)));
  174. // add the last segment for BM.Polygon
  175. if (layer instanceof BM.Polygon) {
  176. // add the last segment for each child that is a nested array
  177. var addLastSegment = function(latlngs) {
  178. if (BM.Polyline._flat(latlngs)) {
  179. latlngs.push(latlngs[0]);
  180. } else {
  181. for (var i = 0; i < latlngs.length; i++) {
  182. addLastSegment(latlngs[i]);
  183. }
  184. }
  185. };
  186. addLastSegment(latlngs);
  187. }
  188. // we have a multi polygon / multi polyline / polygon with holes
  189. // use recursive to explore and return the good result
  190. if ( ! BM.Polyline._flat(latlngs) ) {
  191. for (i = 0; i < latlngs.length; i++) {
  192. // if we are at the lower level, and if we have a BM.Polygon, we add the last segment
  193. subResult = BM.GeometryUtil.closest(map, latlngs[i], latlng, vertices);
  194. if (subResult.distance < mindist) {
  195. mindist = subResult.distance;
  196. result = subResult;
  197. }
  198. }
  199. return result;
  200. } else {
  201. // Lookup vertices
  202. if (vertices) {
  203. for(i = 0, n = latlngs.length; i < n; i++) {
  204. var ll = latlngs[i];
  205. distance = BM.GeometryUtil.distance(map, latlng, ll);
  206. if (distance < mindist) {
  207. mindist = distance;
  208. result = ll;
  209. result.distance = distance;
  210. }
  211. }
  212. return result;
  213. }
  214. // Keep the closest point of all segments
  215. for (i = 0, n = latlngs.length; i < n-1; i++) {
  216. var latlngA = latlngs[i],
  217. latlngB = latlngs[i+1];
  218. distance = BM.GeometryUtil.distanceSegment(map, latlng, latlngA, latlngB);
  219. if (distance <= mindist) {
  220. mindist = distance;
  221. result = BM.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
  222. result.distance = distance;
  223. }
  224. }
  225. return result;
  226. }
  227. },
  228. /**
  229. Returns the closest layer to latlng among a list of layers.
  230. @tutorial closest
  231. @param {BM.Map} map map to be used for this method
  232. @param {Array<BM.ILayer>} layers Set of layers
  233. @param {BM.LatLng} latlng - The position to search
  234. @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty;
  235. */
  236. closestLayer: function (map, layers, latlng) {
  237. var mindist = Infinity,
  238. result = null,
  239. ll = null,
  240. distance = Infinity;
  241. for (var i = 0, n = layers.length; i < n; i++) {
  242. var layer = layers[i];
  243. if (layer instanceof BM.LayerGroup) {
  244. // recursive
  245. var subResult = BM.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
  246. if (subResult.distance < mindist) {
  247. mindist = subResult.distance;
  248. result = subResult;
  249. }
  250. } else {
  251. // Single dimension, snap on points, else snap on closest
  252. if (typeof layer.getLatLng == 'function') {
  253. ll = layer.getLatLng();
  254. distance = BM.GeometryUtil.distance(map, latlng, ll);
  255. }
  256. else {
  257. ll = BM.GeometryUtil.closest(map, layer, latlng);
  258. if (ll) distance = ll.distance; // Can return null if layer has no points.
  259. }
  260. if (distance < mindist) {
  261. mindist = distance;
  262. result = {layer: layer, latlng: ll, distance: distance};
  263. }
  264. }
  265. }
  266. return result;
  267. },
  268. /**
  269. Returns the n closest layers to latlng among a list of input layers.
  270. @param {BM.Map} map - map to be used for this method
  271. @param {Array<BM.ILayer>} layers - Set of layers
  272. @param {BM.LatLng} latlng - The position to search
  273. @param {?Number} [n=layers.length] - the expected number of output layers.
  274. @returns {Array<object>} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n)
  275. */
  276. nClosestLayers: function (map, layers, latlng, n) {
  277. n = typeof n === 'number' ? n : layers.length;
  278. if (n < 1 || layers.length < 1) {
  279. return null;
  280. }
  281. var results = [];
  282. var distance, ll;
  283. for (var i = 0, m = layers.length; i < m; i++) {
  284. var layer = layers[i];
  285. if (layer instanceof BM.LayerGroup) {
  286. // recursive
  287. var subResult = BM.GeometryUtil.closestLayer(map, layer.getLayers(), latlng);
  288. results.push(subResult);
  289. } else {
  290. // Single dimension, snap on points, else snap on closest
  291. if (typeof layer.getLatLng == 'function') {
  292. ll = layer.getLatLng();
  293. distance = BM.GeometryUtil.distance(map, latlng, ll);
  294. }
  295. else {
  296. ll = BM.GeometryUtil.closest(map, layer, latlng);
  297. if (ll) distance = ll.distance; // Can return null if layer has no points.
  298. }
  299. results.push({layer: layer, latlng: ll, distance: distance});
  300. }
  301. }
  302. results.sort(function(a, b) {
  303. return a.distance - b.distance;
  304. });
  305. if (results.length > n) {
  306. return results.slice(0, n);
  307. } else {
  308. return results;
  309. }
  310. },
  311. /**
  312. * Returns all layers within a radius of the given position, in an ascending order of distance.
  313. @param {BM.Map} map map to be used for this method
  314. @param {Array<ILayer>} layers - A list of layers.
  315. @param {BM.LatLng} latlng - The position to search
  316. @param {?Number} [radius=Infinity] - Search radius in pixels
  317. @return {object[]} an array of objects including layer within the radius, closest latlng, and distance
  318. */
  319. layersWithin: function(map, layers, latlng, radius) {
  320. radius = typeof radius == 'number' ? radius : Infinity;
  321. var results = [];
  322. var ll = null;
  323. var distance = 0;
  324. for (var i = 0, n = layers.length; i < n; i++) {
  325. var layer = layers[i];
  326. if (typeof layer.getLatLng == 'function') {
  327. ll = layer.getLatLng();
  328. distance = BM.GeometryUtil.distance(map, latlng, ll);
  329. }
  330. else {
  331. ll = BM.GeometryUtil.closest(map, layer, latlng);
  332. if (ll) distance = ll.distance; // Can return null if layer has no points.
  333. }
  334. if (ll && distance < radius) {
  335. results.push({layer: layer, latlng: ll, distance: distance});
  336. }
  337. }
  338. var sortedResults = results.sort(function(a, b) {
  339. return a.distance - b.distance;
  340. });
  341. return sortedResults;
  342. },
  343. /**
  344. Returns the closest position from specified {LatLng} among specified layers,
  345. with a maximum tolerance in pixels, providing snapping behaviour.
  346. @tutorial closest
  347. @param {BM.Map} map map to be used for this method
  348. @param {Array<ILayer>} layers - A list of layers to snap on.
  349. @param {BM.LatLng} latlng - The position to snap
  350. @param {?Number} [tolerance=Infinity] - Maximum number of pixels.
  351. @param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex)
  352. @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded.
  353. */
  354. closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) {
  355. tolerance = typeof tolerance == 'number' ? tolerance : Infinity;
  356. withVertices = typeof withVertices == 'boolean' ? withVertices : true;
  357. var result = BM.GeometryUtil.closestLayer(map, layers, latlng);
  358. if (!result || result.distance > tolerance)
  359. return null;
  360. // If snapped layer is linear, try to snap on vertices (extremities and middle points)
  361. if (withVertices && typeof result.layer.getLatLngs == 'function') {
  362. var closest = BM.GeometryUtil.closest(map, result.layer, result.latlng, true);
  363. if (closest.distance < tolerance) {
  364. result.latlng = closest;
  365. result.distance = BM.GeometryUtil.distance(map, closest, latlng);
  366. }
  367. }
  368. return result;
  369. },
  370. /**
  371. Returns the Point located on a segment at the specified ratio of the segment length.
  372. @param {BM.Point} pA coordinates of point A
  373. @param {BM.Point} pB coordinates of point B
  374. @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive.
  375. @returns {BM.Point} the interpolated point.
  376. */
  377. interpolateOnPointSegment: function (pA, pB, ratio) {
  378. return BM.point(
  379. (pA.x * (1 - ratio)) + (ratio * pB.x),
  380. (pA.y * (1 - ratio)) + (ratio * pB.y)
  381. );
  382. },
  383. /**
  384. Returns the coordinate of the point located on a line at the specified ratio of the line length.
  385. @param {BM.Map} map map to be used for this method
  386. @param {Array<BM.LatLng>|BM.PolyLine} latlngs Set of geographical points
  387. @param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive
  388. @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline
  389. (-1 if the interpolated point is the first vertex)
  390. */
  391. interpolateOnLine: function (map, latLngs, ratio) {
  392. latLngs = (latLngs instanceof BM.Polyline) ? latLngs.getLatLngs() : latLngs;
  393. var n = latLngs.length;
  394. if (n < 2) {
  395. return null;
  396. }
  397. // ensure the ratio is between 0 and 1;
  398. ratio = Math.max(Math.min(ratio, 1), 0);
  399. if (ratio === 0) {
  400. return {
  401. latLng: latLngs[0] instanceof BM.LatLng ? latLngs[0] : BM.latLng(latLngs[0]),
  402. predecessor: -1
  403. };
  404. }
  405. if (ratio == 1) {
  406. return {
  407. latLng: latLngs[latLngs.length -1] instanceof BM.LatLng ? latLngs[latLngs.length -1] : BM.latLng(latLngs[latLngs.length -1]),
  408. predecessor: latLngs.length - 2
  409. };
  410. }
  411. // project the LatLngs as Points,
  412. // and compute total planar length of the line at max precision
  413. var maxzoom = map.getMaxZoom();
  414. if (maxzoom === Infinity)
  415. maxzoom = map.getZoom();
  416. var pts = [];
  417. var lineLength = 0;
  418. for(var i = 0; i < n; i++) {
  419. pts[i] = map.project(latLngs[i], maxzoom);
  420. if(i > 0)
  421. lineLength += pts[i-1].distanceTo(pts[i]);
  422. }
  423. var ratioDist = lineLength * ratio;
  424. // follow the line segments [ab], adding lengths,
  425. // until we find the segment where the points should lie on
  426. var cumulativeDistanceToA = 0, cumulativeDistanceToB = 0;
  427. for (var i = 0; cumulativeDistanceToB < ratioDist; i++) {
  428. var pointA = pts[i], pointB = pts[i+1];
  429. cumulativeDistanceToA = cumulativeDistanceToB;
  430. cumulativeDistanceToB += pointA.distanceTo(pointB);
  431. }
  432. if (pointA == undefined && pointB == undefined) { // Happens when line has no length
  433. var pointA = pts[0], pointB = pts[1], i = 1;
  434. }
  435. // compute the ratio relative to the segment [ab]
  436. var segmentRatio = ((cumulativeDistanceToB - cumulativeDistanceToA) !== 0) ? ((ratioDist - cumulativeDistanceToA) / (cumulativeDistanceToB - cumulativeDistanceToA)) : 0;
  437. var interpolatedPoint = BM.GeometryUtil.interpolateOnPointSegment(pointA, pointB, segmentRatio);
  438. return {
  439. latLng: map.unproject(interpolatedPoint, maxzoom),
  440. predecessor: i-1
  441. };
  442. },
  443. /**
  444. Returns a float between 0 and 1 representing the location of the
  445. closest point on polyline to the given latlng, as a fraction of total line length.
  446. (opposite of BM.GeometryUtil.interpolateOnLine())
  447. @param {BM.Map} map map to be used for this method
  448. @param {BM.PolyLine} polyline Polyline on which the latlng will be search
  449. @param {BM.LatLng} latlng The position to search
  450. @returns {Number} Float between 0 and 1
  451. */
  452. locateOnLine: function (map, polyline, latlng) {
  453. var latlngs = polyline.getLatLngs();
  454. if (latlng.equals(latlngs[0]))
  455. return 0.0;
  456. if (latlng.equals(latlngs[latlngs.length-1]))
  457. return 1.0;
  458. var point = BM.GeometryUtil.closest(map, polyline, latlng, false),
  459. lengths = BM.GeometryUtil.accumulatedLengths(latlngs),
  460. total_length = lengths[lengths.length-1],
  461. portion = 0,
  462. found = false;
  463. for (var i=0, n = latlngs.length-1; i < n; i++) {
  464. var l1 = latlngs[i],
  465. l2 = latlngs[i+1];
  466. portion = lengths[i];
  467. if (BM.GeometryUtil.belongsSegment(point, l1, l2, 0.0001)) {
  468. portion += l1.distanceTo(point);
  469. found = true;
  470. break;
  471. }
  472. }
  473. if (!found) {
  474. throw "Could not interpolate " + latlng.toString() + " within " + polyline.toString();
  475. }
  476. return portion / total_length;
  477. },
  478. /**
  479. Returns a clone with reversed coordinates.
  480. @param {BM.PolyLine} polyline polyline to reverse
  481. @returns {BM.PolyLine} polyline reversed
  482. */
  483. reverse: function (polyline) {
  484. return BM.polyline(polyline.getLatLngs().slice(0).reverse());
  485. },
  486. /**
  487. Returns a sub-part of the polyline, from start to end.
  488. If start is superior to end, returns extraction from inverted line.
  489. @param {BM.Map} map map to be used for this method
  490. @param {BM.PolyLine} polyline Polyline on which will be extracted the sub-part
  491. @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive
  492. @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive
  493. @returns {Array<BM.LatLng>} new polyline
  494. */
  495. extract: function (map, polyline, start, end) {
  496. if (start > end) {
  497. return BM.GeometryUtil.extract(map, BM.GeometryUtil.reverse(polyline), 1.0-start, 1.0-end);
  498. }
  499. // Bound start and end to [0-1]
  500. start = Math.max(Math.min(start, 1), 0);
  501. end = Math.max(Math.min(end, 1), 0);
  502. var latlngs = polyline.getLatLngs(),
  503. startpoint = BM.GeometryUtil.interpolateOnLine(map, polyline, start),
  504. endpoint = BM.GeometryUtil.interpolateOnLine(map, polyline, end);
  505. // Return single point if start == end
  506. if (start == end) {
  507. var point = BM.GeometryUtil.interpolateOnLine(map, polyline, end);
  508. return [point.latLng];
  509. }
  510. // Array.slice() works indexes at 0
  511. if (startpoint.predecessor == -1)
  512. startpoint.predecessor = 0;
  513. if (endpoint.predecessor == -1)
  514. endpoint.predecessor = 0;
  515. var result = latlngs.slice(startpoint.predecessor+1, endpoint.predecessor+1);
  516. result.unshift(startpoint.latLng);
  517. result.push(endpoint.latLng);
  518. return result;
  519. },
  520. /**
  521. Returns true if first polyline ends where other second starts.
  522. @param {BM.PolyLine} polyline First polyline
  523. @param {BM.PolyLine} other Second polyline
  524. @returns {bool}
  525. */
  526. isBefore: function (polyline, other) {
  527. if (!other) return false;
  528. var lla = polyline.getLatLngs(),
  529. llb = other.getLatLngs();
  530. return (lla[lla.length-1]).equals(llb[0]);
  531. },
  532. /**
  533. Returns true if first polyline starts where second ends.
  534. @param {BM.PolyLine} polyline First polyline
  535. @param {BM.PolyLine} other Second polyline
  536. @returns {bool}
  537. */
  538. isAfter: function (polyline, other) {
  539. if (!other) return false;
  540. var lla = polyline.getLatLngs(),
  541. llb = other.getLatLngs();
  542. return (lla[0]).equals(llb[llb.length-1]);
  543. },
  544. /**
  545. Returns true if first polyline starts where second ends or start.
  546. @param {BM.PolyLine} polyline First polyline
  547. @param {BM.PolyLine} other Second polyline
  548. @returns {bool}
  549. */
  550. startsAtExtremity: function (polyline, other) {
  551. if (!other) return false;
  552. var lla = polyline.getLatLngs(),
  553. llb = other.getLatLngs(),
  554. start = lla[0];
  555. return start.equals(llb[0]) || start.equals(llb[llb.length-1]);
  556. },
  557. /**
  558. Returns horizontal angle in degres between two points.
  559. @param {BM.Point} a Coordinates of point A
  560. @param {BM.Point} b Coordinates of point B
  561. @returns {Number} horizontal angle
  562. */
  563. computeAngle: function(a, b) {
  564. return (Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI);
  565. },
  566. /**
  567. Returns slope (Ax+B) between two points.
  568. @param {BM.Point} a Coordinates of point A
  569. @param {BM.Point} b Coordinates of point B
  570. @returns {Object} with ``a`` and ``b`` properties.
  571. */
  572. computeSlope: function(a, b) {
  573. var s = (b.y - a.y) / (b.x - a.x),
  574. o = a.y - (s * a.x);
  575. return {'a': s, 'b': o};
  576. },
  577. /**
  578. Returns LatLng of rotated point around specified LatLng center.
  579. @param {BM.LatLng} latlngPoint: point to rotate
  580. @param {double} angleDeg: angle to rotate in degrees
  581. @param {BM.LatLng} latlngCenter: center of rotation
  582. @returns {BM.LatLng} rotated point
  583. */
  584. rotatePoint: function(map, latlngPoint, angleDeg, latlngCenter) {
  585. var maxzoom = map.getMaxZoom();
  586. if (maxzoom === Infinity)
  587. maxzoom = map.getZoom();
  588. var angleRad = angleDeg*Math.PI/180,
  589. pPoint = map.project(latlngPoint, maxzoom),
  590. pCenter = map.project(latlngCenter, maxzoom),
  591. x2 = Math.cos(angleRad)*(pPoint.x-pCenter.x) - Math.sin(angleRad)*(pPoint.y-pCenter.y) + pCenter.x,
  592. y2 = Math.sin(angleRad)*(pPoint.x-pCenter.x) + Math.cos(angleRad)*(pPoint.y-pCenter.y) + pCenter.y;
  593. return map.unproject(new BM.Point(x2,y2), maxzoom);
  594. },
  595. /**
  596. Returns the bearing in degrees clockwise from north (0 degrees)
  597. from the first BM.LatLng to the second, at the first LatLng
  598. @param {BM.LatLng} latlng1: origin point of the bearing
  599. @param {BM.LatLng} latlng2: destination point of the bearing
  600. @returns {float} degrees clockwise from north.
  601. */
  602. bearing: function(latlng1, latlng2) {
  603. var rad = Math.PI / 180,
  604. lat1 = latlng1.lat * rad,
  605. lat2 = latlng2.lat * rad,
  606. lon1 = latlng1.lng * rad,
  607. lon2 = latlng2.lng * rad,
  608. y = Math.sin(lon2 - lon1) * Math.cos(lat2),
  609. x = Math.cos(lat1) * Math.sin(lat2) -
  610. Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
  611. var bearing = ((Math.atan2(y, x) * 180 / Math.PI) + 360) % 360;
  612. return bearing >= 180 ? bearing-360 : bearing;
  613. },
  614. /**
  615. Returns the point that is a distance and heading away from
  616. the given origin point.
  617. @param {BM.LatLng} latlng: origin point
  618. @param {float} heading: heading in degrees, clockwise from 0 degrees north.
  619. @param {float} distance: distance in meters
  620. @returns {BM.latLng} the destination point.
  621. Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html
  622. for a great reference and examples.
  623. */
  624. destination: function(latlng, heading, distance) {
  625. heading = (heading + 360) % 360;
  626. var rad = Math.PI / 180,
  627. radInv = 180 / Math.PI,
  628. R = 6378137, // approximation of Earth's radius
  629. lon1 = latlng.lng * rad,
  630. lat1 = latlng.lat * rad,
  631. rheading = heading * rad,
  632. sinLat1 = Math.sin(lat1),
  633. cosLat1 = Math.cos(lat1),
  634. cosDistR = Math.cos(distance / R),
  635. sinDistR = Math.sin(distance / R),
  636. lat2 = Math.asin(sinLat1 * cosDistR + cosLat1 *
  637. sinDistR * Math.cos(rheading)),
  638. lon2 = lon1 + Math.atan2(Math.sin(rheading) * sinDistR *
  639. cosLat1, cosDistR - sinLat1 * Math.sin(lat2));
  640. lon2 = lon2 * radInv;
  641. lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2;
  642. return BM.latLng([lat2 * radInv, lon2]);
  643. },
  644. /**
  645. Returns the the angle of the given segment and the Equator in degrees,
  646. clockwise from 0 degrees north.
  647. @param {BM.Map} map: map to be used for this method
  648. @param {BM.LatLng} latlngA: geographical point A of the segment
  649. @param {BM.LatLng} latlngB: geographical point B of the segment
  650. @returns {Float} the angle in degrees.
  651. */
  652. angle: function(map, latlngA, latlngB) {
  653. var pointA = map.latLngToContainerPoint(latlngA),
  654. pointB = map.latLngToContainerPoint(latlngB),
  655. angleDeg = Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180 / Math.PI + 90;
  656. angleDeg += angleDeg < 0 ? 360 : 0;
  657. return angleDeg;
  658. },
  659. /**
  660. Returns a point snaps on the segment and heading away from the given origin point a distance.
  661. @param {BM.Map} map: map to be used for this method
  662. @param {BM.LatLng} latlngA: geographical point A of the segment
  663. @param {BM.LatLng} latlngB: geographical point B of the segment
  664. @param {float} distance: distance in meters
  665. @returns {BM.latLng} the destination point.
  666. */
  667. destinationOnSegment: function(map, latlngA, latlngB, distance) {
  668. var angleDeg = BM.GeometryUtil.angle(map, latlngA, latlngB),
  669. latlng = BM.GeometryUtil.destination(latlngA, angleDeg, distance);
  670. return BM.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB);
  671. },
  672. });
  673. return BM.GeometryUtil;
  674. }));