PathfinderAlgorithms.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. /* *
  2. *
  3. * (c) 2016 Highsoft AS
  4. * Author: Øystein Moseng
  5. *
  6. * License: www.highcharts.com/license
  7. *
  8. * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
  9. *
  10. * */
  11. 'use strict';
  12. import U from '../Core/Utilities.js';
  13. var extend = U.extend, pick = U.pick;
  14. var min = Math.min, max = Math.max, abs = Math.abs;
  15. /**
  16. * Get index of last obstacle before xMin. Employs a type of binary search, and
  17. * thus requires that obstacles are sorted by xMin value.
  18. *
  19. * @private
  20. * @function findLastObstacleBefore
  21. *
  22. * @param {Array<object>} obstacles
  23. * Array of obstacles to search in.
  24. *
  25. * @param {number} xMin
  26. * The xMin threshold.
  27. *
  28. * @param {number} [startIx]
  29. * Starting index to search from. Must be within array range.
  30. *
  31. * @return {number}
  32. * The index of the last obstacle element before xMin.
  33. */
  34. function findLastObstacleBefore(obstacles, xMin, startIx) {
  35. var left = startIx || 0, // left limit
  36. right = obstacles.length - 1, // right limit
  37. min = xMin - 0.0000001, // Make sure we include all obstacles at xMin
  38. cursor, cmp;
  39. while (left <= right) {
  40. cursor = (right + left) >> 1;
  41. cmp = min - obstacles[cursor].xMin;
  42. if (cmp > 0) {
  43. left = cursor + 1;
  44. }
  45. else if (cmp < 0) {
  46. right = cursor - 1;
  47. }
  48. else {
  49. return cursor;
  50. }
  51. }
  52. return left > 0 ? left - 1 : 0;
  53. }
  54. /**
  55. * Test if a point lays within an obstacle.
  56. *
  57. * @private
  58. * @function pointWithinObstacle
  59. *
  60. * @param {object} obstacle
  61. * Obstacle to test.
  62. *
  63. * @param {Highcharts.Point} point
  64. * Point with x/y props.
  65. *
  66. * @return {boolean}
  67. * Whether point is within the obstacle or not.
  68. */
  69. function pointWithinObstacle(obstacle, point) {
  70. return (point.x <= obstacle.xMax &&
  71. point.x >= obstacle.xMin &&
  72. point.y <= obstacle.yMax &&
  73. point.y >= obstacle.yMin);
  74. }
  75. /**
  76. * Find the index of an obstacle that wraps around a point.
  77. * Returns -1 if not found.
  78. *
  79. * @private
  80. * @function findObstacleFromPoint
  81. *
  82. * @param {Array<object>} obstacles
  83. * Obstacles to test.
  84. *
  85. * @param {Highcharts.Point} point
  86. * Point with x/y props.
  87. *
  88. * @return {number}
  89. * Ix of the obstacle in the array, or -1 if not found.
  90. */
  91. function findObstacleFromPoint(obstacles, point) {
  92. var i = findLastObstacleBefore(obstacles, point.x + 1) + 1;
  93. while (i--) {
  94. if (obstacles[i].xMax >= point.x &&
  95. // optimization using lazy evaluation
  96. pointWithinObstacle(obstacles[i], point)) {
  97. return i;
  98. }
  99. }
  100. return -1;
  101. }
  102. /**
  103. * Get SVG path array from array of line segments.
  104. *
  105. * @private
  106. * @function pathFromSegments
  107. *
  108. * @param {Array<object>} segments
  109. * The segments to build the path from.
  110. *
  111. * @return {Highcharts.SVGPathArray}
  112. * SVG path array as accepted by the SVG Renderer.
  113. */
  114. function pathFromSegments(segments) {
  115. var path = [];
  116. if (segments.length) {
  117. path.push(['M', segments[0].start.x, segments[0].start.y]);
  118. for (var i = 0; i < segments.length; ++i) {
  119. path.push(['L', segments[i].end.x, segments[i].end.y]);
  120. }
  121. }
  122. return path;
  123. }
  124. /**
  125. * Limits obstacle max/mins in all directions to bounds. Modifies input
  126. * obstacle.
  127. *
  128. * @private
  129. * @function limitObstacleToBounds
  130. *
  131. * @param {object} obstacle
  132. * Obstacle to limit.
  133. *
  134. * @param {object} bounds
  135. * Bounds to use as limit.
  136. *
  137. * @return {void}
  138. */
  139. function limitObstacleToBounds(obstacle, bounds) {
  140. obstacle.yMin = max(obstacle.yMin, bounds.yMin);
  141. obstacle.yMax = min(obstacle.yMax, bounds.yMax);
  142. obstacle.xMin = max(obstacle.xMin, bounds.xMin);
  143. obstacle.xMax = min(obstacle.xMax, bounds.xMax);
  144. }
  145. /**
  146. * Get an SVG path from a starting coordinate to an ending coordinate.
  147. * Draws a straight line.
  148. *
  149. * @function Highcharts.Pathfinder.algorithms.straight
  150. *
  151. * @param {Highcharts.PositionObject} start
  152. * Starting coordinate, object with x/y props.
  153. *
  154. * @param {Highcharts.PositionObject} end
  155. * Ending coordinate, object with x/y props.
  156. *
  157. * @return {object}
  158. * An object with the SVG path in Array form as accepted by the SVG
  159. * renderer, as well as an array of new obstacles making up this
  160. * path.
  161. */
  162. function straight(start, end) {
  163. return {
  164. path: [
  165. ['M', start.x, start.y],
  166. ['L', end.x, end.y]
  167. ],
  168. obstacles: [{ start: start, end: end }]
  169. };
  170. }
  171. /**
  172. * Find a path from a starting coordinate to an ending coordinate, using
  173. * right angles only, and taking only starting/ending obstacle into
  174. * consideration.
  175. *
  176. * @function Highcharts.Pathfinder.algorithms.simpleConnect
  177. *
  178. * @param {Highcharts.PositionObject} start
  179. * Starting coordinate, object with x/y props.
  180. *
  181. * @param {Highcharts.PositionObject} end
  182. * Ending coordinate, object with x/y props.
  183. *
  184. * @param {object} options
  185. * Options for the algorithm:
  186. * - chartObstacles: Array of chart obstacles to avoid
  187. * - startDirectionX: Optional. True if starting in the X direction.
  188. * If not provided, the algorithm starts in the direction that is
  189. * the furthest between start/end.
  190. *
  191. * @return {object}
  192. * An object with the SVG path in Array form as accepted by the SVG
  193. * renderer, as well as an array of new obstacles making up this
  194. * path.
  195. */
  196. var simpleConnect = extend(function (start, end, options) {
  197. var segments = [], endSegment, dir = pick(options.startDirectionX, abs(end.x - start.x) > abs(end.y - start.y)) ? 'x' : 'y', chartObstacles = options.chartObstacles, startObstacleIx = findObstacleFromPoint(chartObstacles, start), endObstacleIx = findObstacleFromPoint(chartObstacles, end), startObstacle, endObstacle, prevWaypoint, waypoint, waypoint2, useMax, endPoint;
  198. // eslint-disable-next-line valid-jsdoc
  199. /**
  200. * Return a clone of a point with a property set from a target object,
  201. * optionally with an offset
  202. * @private
  203. */
  204. function copyFromPoint(from, fromKey, to, toKey, offset) {
  205. var point = {
  206. x: from.x,
  207. y: from.y
  208. };
  209. point[fromKey] = to[toKey || fromKey] + (offset || 0);
  210. return point;
  211. }
  212. // eslint-disable-next-line valid-jsdoc
  213. /**
  214. * Return waypoint outside obstacle.
  215. * @private
  216. */
  217. function getMeOut(obstacle, point, direction) {
  218. var useMax = abs(point[direction] - obstacle[direction + 'Min']) >
  219. abs(point[direction] - obstacle[direction + 'Max']);
  220. return copyFromPoint(point, direction, obstacle, direction + (useMax ? 'Max' : 'Min'), useMax ? 1 : -1);
  221. }
  222. // Pull out end point
  223. if (endObstacleIx > -1) {
  224. endObstacle = chartObstacles[endObstacleIx];
  225. waypoint = getMeOut(endObstacle, end, dir);
  226. endSegment = {
  227. start: waypoint,
  228. end: end
  229. };
  230. endPoint = waypoint;
  231. }
  232. else {
  233. endPoint = end;
  234. }
  235. // If an obstacle envelops the start point, add a segment to get out,
  236. // and around it.
  237. if (startObstacleIx > -1) {
  238. startObstacle = chartObstacles[startObstacleIx];
  239. waypoint = getMeOut(startObstacle, start, dir);
  240. segments.push({
  241. start: start,
  242. end: waypoint
  243. });
  244. // If we are going back again, switch direction to get around start
  245. // obstacle.
  246. if (
  247. // Going towards max from start:
  248. waypoint[dir] >= start[dir] ===
  249. // Going towards min to end:
  250. waypoint[dir] >= endPoint[dir]) {
  251. dir = dir === 'y' ? 'x' : 'y';
  252. useMax = start[dir] < end[dir];
  253. segments.push({
  254. start: waypoint,
  255. end: copyFromPoint(waypoint, dir, startObstacle, dir + (useMax ? 'Max' : 'Min'), useMax ? 1 : -1)
  256. });
  257. // Switch direction again
  258. dir = dir === 'y' ? 'x' : 'y';
  259. }
  260. }
  261. // We are around the start obstacle. Go towards the end in one
  262. // direction.
  263. prevWaypoint = segments.length ?
  264. segments[segments.length - 1].end :
  265. start;
  266. waypoint = copyFromPoint(prevWaypoint, dir, endPoint);
  267. segments.push({
  268. start: prevWaypoint,
  269. end: waypoint
  270. });
  271. // Final run to end point in the other direction
  272. dir = dir === 'y' ? 'x' : 'y';
  273. waypoint2 = copyFromPoint(waypoint, dir, endPoint);
  274. segments.push({
  275. start: waypoint,
  276. end: waypoint2
  277. });
  278. // Finally add the endSegment
  279. segments.push(endSegment);
  280. return {
  281. path: pathFromSegments(segments),
  282. obstacles: segments
  283. };
  284. }, {
  285. requiresObstacles: true
  286. });
  287. /**
  288. * Find a path from a starting coordinate to an ending coordinate, taking
  289. * obstacles into consideration. Might not always find the optimal path,
  290. * but is fast, and usually good enough.
  291. *
  292. * @function Highcharts.Pathfinder.algorithms.fastAvoid
  293. *
  294. * @param {Highcharts.PositionObject} start
  295. * Starting coordinate, object with x/y props.
  296. *
  297. * @param {Highcharts.PositionObject} end
  298. * Ending coordinate, object with x/y props.
  299. *
  300. * @param {object} options
  301. * Options for the algorithm.
  302. * - chartObstacles: Array of chart obstacles to avoid
  303. * - lineObstacles: Array of line obstacles to jump over
  304. * - obstacleMetrics: Object with metrics of chartObstacles cached
  305. * - hardBounds: Hard boundaries to not cross
  306. * - obstacleOptions: Options for the obstacles, including margin
  307. * - startDirectionX: Optional. True if starting in the X direction.
  308. * If not provided, the algorithm starts in the
  309. * direction that is the furthest between
  310. * start/end.
  311. *
  312. * @return {object}
  313. * An object with the SVG path in Array form as accepted by the SVG
  314. * renderer, as well as an array of new obstacles making up this
  315. * path.
  316. */
  317. var fastAvoid = extend(function (start, end, options) {
  318. /*
  319. Algorithm rules/description
  320. - Find initial direction
  321. - Determine soft/hard max for each direction.
  322. - Move along initial direction until obstacle.
  323. - Change direction.
  324. - If hitting obstacle, first try to change length of previous line
  325. before changing direction again.
  326. Soft min/max x = start/destination x +/- widest obstacle + margin
  327. Soft min/max y = start/destination y +/- tallest obstacle + margin
  328. @todo:
  329. - Make retrospective, try changing prev segment to reduce
  330. corners
  331. - Fix logic for breaking out of end-points - not always picking
  332. the best direction currently
  333. - When going around the end obstacle we should not always go the
  334. shortest route, rather pick the one closer to the end point
  335. */
  336. var dirIsX = pick(options.startDirectionX, abs(end.x - start.x) > abs(end.y - start.y)), dir = dirIsX ? 'x' : 'y', segments, useMax, extractedEndPoint, endSegments = [], forceObstacleBreak = false, // Used in clearPathTo to keep track of
  337. // when to force break through an obstacle.
  338. // Boundaries to stay within. If beyond soft boundary, prefer to
  339. // change direction ASAP. If at hard max, always change immediately.
  340. metrics = options.obstacleMetrics, softMinX = min(start.x, end.x) - metrics.maxWidth - 10, softMaxX = max(start.x, end.x) + metrics.maxWidth + 10, softMinY = min(start.y, end.y) - metrics.maxHeight - 10, softMaxY = max(start.y, end.y) + metrics.maxHeight + 10,
  341. // Obstacles
  342. chartObstacles = options.chartObstacles, startObstacleIx = findLastObstacleBefore(chartObstacles, softMinX), endObstacleIx = findLastObstacleBefore(chartObstacles, softMaxX);
  343. // eslint-disable-next-line valid-jsdoc
  344. /**
  345. * How far can you go between two points before hitting an obstacle?
  346. * Does not work for diagonal lines (because it doesn't have to).
  347. * @private
  348. */
  349. function pivotPoint(fromPoint, toPoint, directionIsX) {
  350. var firstPoint, lastPoint, highestPoint, lowestPoint, i, searchDirection = fromPoint.x < toPoint.x ? 1 : -1;
  351. if (fromPoint.x < toPoint.x) {
  352. firstPoint = fromPoint;
  353. lastPoint = toPoint;
  354. }
  355. else {
  356. firstPoint = toPoint;
  357. lastPoint = fromPoint;
  358. }
  359. if (fromPoint.y < toPoint.y) {
  360. lowestPoint = fromPoint;
  361. highestPoint = toPoint;
  362. }
  363. else {
  364. lowestPoint = toPoint;
  365. highestPoint = fromPoint;
  366. }
  367. // Go through obstacle range in reverse if toPoint is before
  368. // fromPoint in the X-dimension.
  369. i = searchDirection < 0 ?
  370. // Searching backwards, start at last obstacle before last point
  371. min(findLastObstacleBefore(chartObstacles, lastPoint.x), chartObstacles.length - 1) :
  372. // Forwards. Since we're not sorted by xMax, we have to look
  373. // at all obstacles.
  374. 0;
  375. // Go through obstacles in this X range
  376. while (chartObstacles[i] && (searchDirection > 0 && chartObstacles[i].xMin <= lastPoint.x ||
  377. searchDirection < 0 && chartObstacles[i].xMax >= firstPoint.x)) {
  378. // If this obstacle is between from and to points in a straight
  379. // line, pivot at the intersection.
  380. if (chartObstacles[i].xMin <= lastPoint.x &&
  381. chartObstacles[i].xMax >= firstPoint.x &&
  382. chartObstacles[i].yMin <= highestPoint.y &&
  383. chartObstacles[i].yMax >= lowestPoint.y) {
  384. if (directionIsX) {
  385. return {
  386. y: fromPoint.y,
  387. x: fromPoint.x < toPoint.x ?
  388. chartObstacles[i].xMin - 1 :
  389. chartObstacles[i].xMax + 1,
  390. obstacle: chartObstacles[i]
  391. };
  392. }
  393. // else ...
  394. return {
  395. x: fromPoint.x,
  396. y: fromPoint.y < toPoint.y ?
  397. chartObstacles[i].yMin - 1 :
  398. chartObstacles[i].yMax + 1,
  399. obstacle: chartObstacles[i]
  400. };
  401. }
  402. i += searchDirection;
  403. }
  404. return toPoint;
  405. }
  406. /**
  407. * Decide in which direction to dodge or get out of an obstacle.
  408. * Considers desired direction, which way is shortest, soft and hard
  409. * bounds.
  410. *
  411. * (? Returns a string, either xMin, xMax, yMin or yMax.)
  412. *
  413. * @private
  414. * @function
  415. *
  416. * @param {object} obstacle
  417. * Obstacle to dodge/escape.
  418. *
  419. * @param {object} fromPoint
  420. * Point with x/y props that's dodging/escaping.
  421. *
  422. * @param {object} toPoint
  423. * Goal point.
  424. *
  425. * @param {boolean} dirIsX
  426. * Dodge in X dimension.
  427. *
  428. * @param {object} bounds
  429. * Hard and soft boundaries.
  430. *
  431. * @return {boolean}
  432. * Use max or not.
  433. */
  434. function getDodgeDirection(obstacle, fromPoint, toPoint, dirIsX, bounds) {
  435. var softBounds = bounds.soft, hardBounds = bounds.hard, dir = dirIsX ? 'x' : 'y', toPointMax = { x: fromPoint.x, y: fromPoint.y }, toPointMin = { x: fromPoint.x, y: fromPoint.y }, minPivot, maxPivot, maxOutOfSoftBounds = obstacle[dir + 'Max'] >=
  436. softBounds[dir + 'Max'], minOutOfSoftBounds = obstacle[dir + 'Min'] <=
  437. softBounds[dir + 'Min'], maxOutOfHardBounds = obstacle[dir + 'Max'] >=
  438. hardBounds[dir + 'Max'], minOutOfHardBounds = obstacle[dir + 'Min'] <=
  439. hardBounds[dir + 'Min'],
  440. // Find out if we should prefer one direction over the other if
  441. // we can choose freely
  442. minDistance = abs(obstacle[dir + 'Min'] - fromPoint[dir]), maxDistance = abs(obstacle[dir + 'Max'] - fromPoint[dir]),
  443. // If it's a small difference, pick the one leading towards dest
  444. // point. Otherwise pick the shortest distance
  445. useMax = abs(minDistance - maxDistance) < 10 ?
  446. fromPoint[dir] < toPoint[dir] :
  447. maxDistance < minDistance;
  448. // Check if we hit any obstacles trying to go around in either
  449. // direction.
  450. toPointMin[dir] = obstacle[dir + 'Min'];
  451. toPointMax[dir] = obstacle[dir + 'Max'];
  452. minPivot = pivotPoint(fromPoint, toPointMin, dirIsX)[dir] !==
  453. toPointMin[dir];
  454. maxPivot = pivotPoint(fromPoint, toPointMax, dirIsX)[dir] !==
  455. toPointMax[dir];
  456. useMax = minPivot ?
  457. (maxPivot ? useMax : true) :
  458. (maxPivot ? false : useMax);
  459. // useMax now contains our preferred choice, bounds not taken into
  460. // account. If both or neither direction is out of bounds we want to
  461. // use this.
  462. // Deal with soft bounds
  463. useMax = minOutOfSoftBounds ?
  464. (maxOutOfSoftBounds ? useMax : true) : // Out on min
  465. (maxOutOfSoftBounds ? false : useMax); // Not out on min
  466. // Deal with hard bounds
  467. useMax = minOutOfHardBounds ?
  468. (maxOutOfHardBounds ? useMax : true) : // Out on min
  469. (maxOutOfHardBounds ? false : useMax); // Not out on min
  470. return useMax;
  471. }
  472. // eslint-disable-next-line valid-jsdoc
  473. /**
  474. * Find a clear path between point.
  475. * @private
  476. */
  477. function clearPathTo(fromPoint, toPoint, dirIsX) {
  478. // Don't waste time if we've hit goal
  479. if (fromPoint.x === toPoint.x && fromPoint.y === toPoint.y) {
  480. return [];
  481. }
  482. var dir = dirIsX ? 'x' : 'y', pivot, segments, waypoint, waypointUseMax, envelopingObstacle, secondEnvelopingObstacle, envelopWaypoint, obstacleMargin = options.obstacleOptions.margin, bounds = {
  483. soft: {
  484. xMin: softMinX,
  485. xMax: softMaxX,
  486. yMin: softMinY,
  487. yMax: softMaxY
  488. },
  489. hard: options.hardBounds
  490. };
  491. // If fromPoint is inside an obstacle we have a problem. Break out
  492. // by just going to the outside of this obstacle. We prefer to go to
  493. // the nearest edge in the chosen direction.
  494. envelopingObstacle =
  495. findObstacleFromPoint(chartObstacles, fromPoint);
  496. if (envelopingObstacle > -1) {
  497. envelopingObstacle = chartObstacles[envelopingObstacle];
  498. waypointUseMax = getDodgeDirection(envelopingObstacle, fromPoint, toPoint, dirIsX, bounds);
  499. // Cut obstacle to hard bounds to make sure we stay within
  500. limitObstacleToBounds(envelopingObstacle, options.hardBounds);
  501. envelopWaypoint = dirIsX ? {
  502. y: fromPoint.y,
  503. x: envelopingObstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  504. (waypointUseMax ? 1 : -1)
  505. } : {
  506. x: fromPoint.x,
  507. y: envelopingObstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  508. (waypointUseMax ? 1 : -1)
  509. };
  510. // If we crashed into another obstacle doing this, we put the
  511. // waypoint between them instead
  512. secondEnvelopingObstacle = findObstacleFromPoint(chartObstacles, envelopWaypoint);
  513. if (secondEnvelopingObstacle > -1) {
  514. secondEnvelopingObstacle = chartObstacles[secondEnvelopingObstacle];
  515. // Cut obstacle to hard bounds
  516. limitObstacleToBounds(secondEnvelopingObstacle, options.hardBounds);
  517. // Modify waypoint to lay between obstacles
  518. envelopWaypoint[dir] = waypointUseMax ? max(envelopingObstacle[dir + 'Max'] - obstacleMargin + 1, (secondEnvelopingObstacle[dir + 'Min'] +
  519. envelopingObstacle[dir + 'Max']) / 2) :
  520. min((envelopingObstacle[dir + 'Min'] + obstacleMargin - 1), ((secondEnvelopingObstacle[dir + 'Max'] +
  521. envelopingObstacle[dir + 'Min']) / 2));
  522. // We are not going anywhere. If this happens for the first
  523. // time, do nothing. Otherwise, try to go to the extreme of
  524. // the obstacle pair in the current direction.
  525. if (fromPoint.x === envelopWaypoint.x &&
  526. fromPoint.y === envelopWaypoint.y) {
  527. if (forceObstacleBreak) {
  528. envelopWaypoint[dir] = waypointUseMax ?
  529. max(envelopingObstacle[dir + 'Max'], secondEnvelopingObstacle[dir + 'Max']) + 1 :
  530. min(envelopingObstacle[dir + 'Min'], secondEnvelopingObstacle[dir + 'Min']) - 1;
  531. }
  532. // Toggle on if off, and the opposite
  533. forceObstacleBreak = !forceObstacleBreak;
  534. }
  535. else {
  536. // This point is not identical to previous.
  537. // Clear break trigger.
  538. forceObstacleBreak = false;
  539. }
  540. }
  541. segments = [{
  542. start: fromPoint,
  543. end: envelopWaypoint
  544. }];
  545. }
  546. else { // If not enveloping, use standard pivot calculation
  547. pivot = pivotPoint(fromPoint, {
  548. x: dirIsX ? toPoint.x : fromPoint.x,
  549. y: dirIsX ? fromPoint.y : toPoint.y
  550. }, dirIsX);
  551. segments = [{
  552. start: fromPoint,
  553. end: {
  554. x: pivot.x,
  555. y: pivot.y
  556. }
  557. }];
  558. // Pivot before goal, use a waypoint to dodge obstacle
  559. if (pivot[dirIsX ? 'x' : 'y'] !== toPoint[dirIsX ? 'x' : 'y']) {
  560. // Find direction of waypoint
  561. waypointUseMax = getDodgeDirection(pivot.obstacle, pivot, toPoint, !dirIsX, bounds);
  562. // Cut waypoint to hard bounds
  563. limitObstacleToBounds(pivot.obstacle, options.hardBounds);
  564. waypoint = {
  565. x: dirIsX ?
  566. pivot.x :
  567. pivot.obstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  568. (waypointUseMax ? 1 : -1),
  569. y: dirIsX ?
  570. pivot.obstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  571. (waypointUseMax ? 1 : -1) :
  572. pivot.y
  573. };
  574. // We're changing direction here, store that to make sure we
  575. // also change direction when adding the last segment array
  576. // after handling waypoint.
  577. dirIsX = !dirIsX;
  578. segments = segments.concat(clearPathTo({
  579. x: pivot.x,
  580. y: pivot.y
  581. }, waypoint, dirIsX));
  582. }
  583. }
  584. // Get segments for the other direction too
  585. // Recursion is our friend
  586. segments = segments.concat(clearPathTo(segments[segments.length - 1].end, toPoint, !dirIsX));
  587. return segments;
  588. }
  589. // eslint-disable-next-line valid-jsdoc
  590. /**
  591. * Extract point to outside of obstacle in whichever direction is
  592. * closest. Returns new point outside obstacle.
  593. * @private
  594. */
  595. function extractFromObstacle(obstacle, point, goalPoint) {
  596. var dirIsX = min(obstacle.xMax - point.x, point.x - obstacle.xMin) <
  597. min(obstacle.yMax - point.y, point.y - obstacle.yMin), bounds = {
  598. soft: options.hardBounds,
  599. hard: options.hardBounds
  600. }, useMax = getDodgeDirection(obstacle, point, goalPoint, dirIsX, bounds);
  601. return dirIsX ? {
  602. y: point.y,
  603. x: obstacle[useMax ? 'xMax' : 'xMin'] + (useMax ? 1 : -1)
  604. } : {
  605. x: point.x,
  606. y: obstacle[useMax ? 'yMax' : 'yMin'] + (useMax ? 1 : -1)
  607. };
  608. }
  609. // Cut the obstacle array to soft bounds for optimization in large
  610. // datasets.
  611. chartObstacles =
  612. chartObstacles.slice(startObstacleIx, endObstacleIx + 1);
  613. // If an obstacle envelops the end point, move it out of there and add
  614. // a little segment to where it was.
  615. if ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
  616. extractedEndPoint = extractFromObstacle(chartObstacles[endObstacleIx], end, start);
  617. endSegments.push({
  618. end: end,
  619. start: extractedEndPoint
  620. });
  621. end = extractedEndPoint;
  622. }
  623. // If it's still inside one or more obstacles, get out of there by
  624. // force-moving towards the start point.
  625. while ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
  626. useMax = end[dir] - start[dir] < 0;
  627. extractedEndPoint = {
  628. x: end.x,
  629. y: end.y
  630. };
  631. extractedEndPoint[dir] = chartObstacles[endObstacleIx][useMax ? dir + 'Max' : dir + 'Min'] + (useMax ? 1 : -1);
  632. endSegments.push({
  633. end: end,
  634. start: extractedEndPoint
  635. });
  636. end = extractedEndPoint;
  637. }
  638. // Find the path
  639. segments = clearPathTo(start, end, dirIsX);
  640. // Add the end-point segments
  641. segments = segments.concat(endSegments.reverse());
  642. return {
  643. path: pathFromSegments(segments),
  644. obstacles: segments
  645. };
  646. }, {
  647. requiresObstacles: true
  648. });
  649. // Define the available pathfinding algorithms.
  650. // Algorithms take up to 3 arguments: starting point, ending point, and an
  651. // options object.
  652. var algorithms = {
  653. fastAvoid: fastAvoid,
  654. straight: straight,
  655. simpleConnect: simpleConnect
  656. };
  657. export default algorithms;