PathfinderAlgorithms.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  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 = 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. simpleConnect.requiresObstacles = true;
  286. /**
  287. * Find a path from a starting coordinate to an ending coordinate, taking
  288. * obstacles into consideration. Might not always find the optimal path,
  289. * but is fast, and usually good enough.
  290. *
  291. * @function Highcharts.Pathfinder.algorithms.fastAvoid
  292. *
  293. * @param {Highcharts.PositionObject} start
  294. * Starting coordinate, object with x/y props.
  295. *
  296. * @param {Highcharts.PositionObject} end
  297. * Ending coordinate, object with x/y props.
  298. *
  299. * @param {object} options
  300. * Options for the algorithm.
  301. * - chartObstacles: Array of chart obstacles to avoid
  302. * - lineObstacles: Array of line obstacles to jump over
  303. * - obstacleMetrics: Object with metrics of chartObstacles cached
  304. * - hardBounds: Hard boundaries to not cross
  305. * - obstacleOptions: Options for the obstacles, including margin
  306. * - startDirectionX: Optional. True if starting in the X direction.
  307. * If not provided, the algorithm starts in the
  308. * direction that is the furthest between
  309. * start/end.
  310. *
  311. * @return {object}
  312. * An object with the SVG path in Array form as accepted by the SVG
  313. * renderer, as well as an array of new obstacles making up this
  314. * path.
  315. */
  316. var fastAvoid = function (start, end, options) {
  317. /*
  318. Algorithm rules/description
  319. - Find initial direction
  320. - Determine soft/hard max for each direction.
  321. - Move along initial direction until obstacle.
  322. - Change direction.
  323. - If hitting obstacle, first try to change length of previous line
  324. before changing direction again.
  325. Soft min/max x = start/destination x +/- widest obstacle + margin
  326. Soft min/max y = start/destination y +/- tallest obstacle + margin
  327. @todo:
  328. - Make retrospective, try changing prev segment to reduce
  329. corners
  330. - Fix logic for breaking out of end-points - not always picking
  331. the best direction currently
  332. - When going around the end obstacle we should not always go the
  333. shortest route, rather pick the one closer to the end point
  334. */
  335. 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
  336. // when to force break through an obstacle.
  337. // Boundaries to stay within. If beyond soft boundary, prefer to
  338. // change direction ASAP. If at hard max, always change immediately.
  339. 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,
  340. // Obstacles
  341. chartObstacles = options.chartObstacles, startObstacleIx = findLastObstacleBefore(chartObstacles, softMinX), endObstacleIx = findLastObstacleBefore(chartObstacles, softMaxX);
  342. // eslint-disable-next-line valid-jsdoc
  343. /**
  344. * How far can you go between two points before hitting an obstacle?
  345. * Does not work for diagonal lines (because it doesn't have to).
  346. * @private
  347. */
  348. function pivotPoint(fromPoint, toPoint, directionIsX) {
  349. var firstPoint, lastPoint, highestPoint, lowestPoint, i, searchDirection = fromPoint.x < toPoint.x ? 1 : -1;
  350. if (fromPoint.x < toPoint.x) {
  351. firstPoint = fromPoint;
  352. lastPoint = toPoint;
  353. }
  354. else {
  355. firstPoint = toPoint;
  356. lastPoint = fromPoint;
  357. }
  358. if (fromPoint.y < toPoint.y) {
  359. lowestPoint = fromPoint;
  360. highestPoint = toPoint;
  361. }
  362. else {
  363. lowestPoint = toPoint;
  364. highestPoint = fromPoint;
  365. }
  366. // Go through obstacle range in reverse if toPoint is before
  367. // fromPoint in the X-dimension.
  368. i = searchDirection < 0 ?
  369. // Searching backwards, start at last obstacle before last point
  370. min(findLastObstacleBefore(chartObstacles, lastPoint.x), chartObstacles.length - 1) :
  371. // Forwards. Since we're not sorted by xMax, we have to look
  372. // at all obstacles.
  373. 0;
  374. // Go through obstacles in this X range
  375. while (chartObstacles[i] && (searchDirection > 0 && chartObstacles[i].xMin <= lastPoint.x ||
  376. searchDirection < 0 && chartObstacles[i].xMax >= firstPoint.x)) {
  377. // If this obstacle is between from and to points in a straight
  378. // line, pivot at the intersection.
  379. if (chartObstacles[i].xMin <= lastPoint.x &&
  380. chartObstacles[i].xMax >= firstPoint.x &&
  381. chartObstacles[i].yMin <= highestPoint.y &&
  382. chartObstacles[i].yMax >= lowestPoint.y) {
  383. if (directionIsX) {
  384. return {
  385. y: fromPoint.y,
  386. x: fromPoint.x < toPoint.x ?
  387. chartObstacles[i].xMin - 1 :
  388. chartObstacles[i].xMax + 1,
  389. obstacle: chartObstacles[i]
  390. };
  391. }
  392. // else ...
  393. return {
  394. x: fromPoint.x,
  395. y: fromPoint.y < toPoint.y ?
  396. chartObstacles[i].yMin - 1 :
  397. chartObstacles[i].yMax + 1,
  398. obstacle: chartObstacles[i]
  399. };
  400. }
  401. i += searchDirection;
  402. }
  403. return toPoint;
  404. }
  405. /**
  406. * Decide in which direction to dodge or get out of an obstacle.
  407. * Considers desired direction, which way is shortest, soft and hard
  408. * bounds.
  409. *
  410. * (? Returns a string, either xMin, xMax, yMin or yMax.)
  411. *
  412. * @private
  413. * @function
  414. *
  415. * @param {object} obstacle
  416. * Obstacle to dodge/escape.
  417. *
  418. * @param {object} fromPoint
  419. * Point with x/y props that's dodging/escaping.
  420. *
  421. * @param {object} toPoint
  422. * Goal point.
  423. *
  424. * @param {boolean} dirIsX
  425. * Dodge in X dimension.
  426. *
  427. * @param {object} bounds
  428. * Hard and soft boundaries.
  429. *
  430. * @return {boolean}
  431. * Use max or not.
  432. */
  433. function getDodgeDirection(obstacle, fromPoint, toPoint, dirIsX, bounds) {
  434. 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'] >=
  435. softBounds[dir + 'Max'], minOutOfSoftBounds = obstacle[dir + 'Min'] <=
  436. softBounds[dir + 'Min'], maxOutOfHardBounds = obstacle[dir + 'Max'] >=
  437. hardBounds[dir + 'Max'], minOutOfHardBounds = obstacle[dir + 'Min'] <=
  438. hardBounds[dir + 'Min'],
  439. // Find out if we should prefer one direction over the other if
  440. // we can choose freely
  441. minDistance = abs(obstacle[dir + 'Min'] - fromPoint[dir]), maxDistance = abs(obstacle[dir + 'Max'] - fromPoint[dir]),
  442. // If it's a small difference, pick the one leading towards dest
  443. // point. Otherwise pick the shortest distance
  444. useMax = abs(minDistance - maxDistance) < 10 ?
  445. fromPoint[dir] < toPoint[dir] :
  446. maxDistance < minDistance;
  447. // Check if we hit any obstacles trying to go around in either
  448. // direction.
  449. toPointMin[dir] = obstacle[dir + 'Min'];
  450. toPointMax[dir] = obstacle[dir + 'Max'];
  451. minPivot = pivotPoint(fromPoint, toPointMin, dirIsX)[dir] !==
  452. toPointMin[dir];
  453. maxPivot = pivotPoint(fromPoint, toPointMax, dirIsX)[dir] !==
  454. toPointMax[dir];
  455. useMax = minPivot ?
  456. (maxPivot ? useMax : true) :
  457. (maxPivot ? false : useMax);
  458. // useMax now contains our preferred choice, bounds not taken into
  459. // account. If both or neither direction is out of bounds we want to
  460. // use this.
  461. // Deal with soft bounds
  462. useMax = minOutOfSoftBounds ?
  463. (maxOutOfSoftBounds ? useMax : true) : // Out on min
  464. (maxOutOfSoftBounds ? false : useMax); // Not out on min
  465. // Deal with hard bounds
  466. useMax = minOutOfHardBounds ?
  467. (maxOutOfHardBounds ? useMax : true) : // Out on min
  468. (maxOutOfHardBounds ? false : useMax); // Not out on min
  469. return useMax;
  470. }
  471. // eslint-disable-next-line valid-jsdoc
  472. /**
  473. * Find a clear path between point.
  474. * @private
  475. */
  476. function clearPathTo(fromPoint, toPoint, dirIsX) {
  477. // Don't waste time if we've hit goal
  478. if (fromPoint.x === toPoint.x && fromPoint.y === toPoint.y) {
  479. return [];
  480. }
  481. var dir = dirIsX ? 'x' : 'y', pivot, segments, waypoint, waypointUseMax, envelopingObstacle, secondEnvelopingObstacle, envelopWaypoint, obstacleMargin = options.obstacleOptions.margin, bounds = {
  482. soft: {
  483. xMin: softMinX,
  484. xMax: softMaxX,
  485. yMin: softMinY,
  486. yMax: softMaxY
  487. },
  488. hard: options.hardBounds
  489. };
  490. // If fromPoint is inside an obstacle we have a problem. Break out
  491. // by just going to the outside of this obstacle. We prefer to go to
  492. // the nearest edge in the chosen direction.
  493. envelopingObstacle =
  494. findObstacleFromPoint(chartObstacles, fromPoint);
  495. if (envelopingObstacle > -1) {
  496. envelopingObstacle = chartObstacles[envelopingObstacle];
  497. waypointUseMax = getDodgeDirection(envelopingObstacle, fromPoint, toPoint, dirIsX, bounds);
  498. // Cut obstacle to hard bounds to make sure we stay within
  499. limitObstacleToBounds(envelopingObstacle, options.hardBounds);
  500. envelopWaypoint = dirIsX ? {
  501. y: fromPoint.y,
  502. x: envelopingObstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  503. (waypointUseMax ? 1 : -1)
  504. } : {
  505. x: fromPoint.x,
  506. y: envelopingObstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  507. (waypointUseMax ? 1 : -1)
  508. };
  509. // If we crashed into another obstacle doing this, we put the
  510. // waypoint between them instead
  511. secondEnvelopingObstacle = findObstacleFromPoint(chartObstacles, envelopWaypoint);
  512. if (secondEnvelopingObstacle > -1) {
  513. secondEnvelopingObstacle = chartObstacles[secondEnvelopingObstacle];
  514. // Cut obstacle to hard bounds
  515. limitObstacleToBounds(secondEnvelopingObstacle, options.hardBounds);
  516. // Modify waypoint to lay between obstacles
  517. envelopWaypoint[dir] = waypointUseMax ? max(envelopingObstacle[dir + 'Max'] - obstacleMargin + 1, (secondEnvelopingObstacle[dir + 'Min'] +
  518. envelopingObstacle[dir + 'Max']) / 2) :
  519. min((envelopingObstacle[dir + 'Min'] + obstacleMargin - 1), ((secondEnvelopingObstacle[dir + 'Max'] +
  520. envelopingObstacle[dir + 'Min']) / 2));
  521. // We are not going anywhere. If this happens for the first
  522. // time, do nothing. Otherwise, try to go to the extreme of
  523. // the obstacle pair in the current direction.
  524. if (fromPoint.x === envelopWaypoint.x &&
  525. fromPoint.y === envelopWaypoint.y) {
  526. if (forceObstacleBreak) {
  527. envelopWaypoint[dir] = waypointUseMax ?
  528. max(envelopingObstacle[dir + 'Max'], secondEnvelopingObstacle[dir + 'Max']) + 1 :
  529. min(envelopingObstacle[dir + 'Min'], secondEnvelopingObstacle[dir + 'Min']) - 1;
  530. }
  531. // Toggle on if off, and the opposite
  532. forceObstacleBreak = !forceObstacleBreak;
  533. }
  534. else {
  535. // This point is not identical to previous.
  536. // Clear break trigger.
  537. forceObstacleBreak = false;
  538. }
  539. }
  540. segments = [{
  541. start: fromPoint,
  542. end: envelopWaypoint
  543. }];
  544. }
  545. else { // If not enveloping, use standard pivot calculation
  546. pivot = pivotPoint(fromPoint, {
  547. x: dirIsX ? toPoint.x : fromPoint.x,
  548. y: dirIsX ? fromPoint.y : toPoint.y
  549. }, dirIsX);
  550. segments = [{
  551. start: fromPoint,
  552. end: {
  553. x: pivot.x,
  554. y: pivot.y
  555. }
  556. }];
  557. // Pivot before goal, use a waypoint to dodge obstacle
  558. if (pivot[dirIsX ? 'x' : 'y'] !== toPoint[dirIsX ? 'x' : 'y']) {
  559. // Find direction of waypoint
  560. waypointUseMax = getDodgeDirection(pivot.obstacle, pivot, toPoint, !dirIsX, bounds);
  561. // Cut waypoint to hard bounds
  562. limitObstacleToBounds(pivot.obstacle, options.hardBounds);
  563. waypoint = {
  564. x: dirIsX ?
  565. pivot.x :
  566. pivot.obstacle[waypointUseMax ? 'xMax' : 'xMin'] +
  567. (waypointUseMax ? 1 : -1),
  568. y: dirIsX ?
  569. pivot.obstacle[waypointUseMax ? 'yMax' : 'yMin'] +
  570. (waypointUseMax ? 1 : -1) :
  571. pivot.y
  572. };
  573. // We're changing direction here, store that to make sure we
  574. // also change direction when adding the last segment array
  575. // after handling waypoint.
  576. dirIsX = !dirIsX;
  577. segments = segments.concat(clearPathTo({
  578. x: pivot.x,
  579. y: pivot.y
  580. }, waypoint, dirIsX));
  581. }
  582. }
  583. // Get segments for the other direction too
  584. // Recursion is our friend
  585. segments = segments.concat(clearPathTo(segments[segments.length - 1].end, toPoint, !dirIsX));
  586. return segments;
  587. }
  588. // eslint-disable-next-line valid-jsdoc
  589. /**
  590. * Extract point to outside of obstacle in whichever direction is
  591. * closest. Returns new point outside obstacle.
  592. * @private
  593. */
  594. function extractFromObstacle(obstacle, point, goalPoint) {
  595. var dirIsX = min(obstacle.xMax - point.x, point.x - obstacle.xMin) <
  596. min(obstacle.yMax - point.y, point.y - obstacle.yMin), bounds = {
  597. soft: options.hardBounds,
  598. hard: options.hardBounds
  599. }, useMax = getDodgeDirection(obstacle, point, goalPoint, dirIsX, bounds);
  600. return dirIsX ? {
  601. y: point.y,
  602. x: obstacle[useMax ? 'xMax' : 'xMin'] + (useMax ? 1 : -1)
  603. } : {
  604. x: point.x,
  605. y: obstacle[useMax ? 'yMax' : 'yMin'] + (useMax ? 1 : -1)
  606. };
  607. }
  608. // Cut the obstacle array to soft bounds for optimization in large
  609. // datasets.
  610. chartObstacles =
  611. chartObstacles.slice(startObstacleIx, endObstacleIx + 1);
  612. // If an obstacle envelops the end point, move it out of there and add
  613. // a little segment to where it was.
  614. if ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
  615. extractedEndPoint = extractFromObstacle(chartObstacles[endObstacleIx], end, start);
  616. endSegments.push({
  617. end: end,
  618. start: extractedEndPoint
  619. });
  620. end = extractedEndPoint;
  621. }
  622. // If it's still inside one or more obstacles, get out of there by
  623. // force-moving towards the start point.
  624. while ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
  625. useMax = end[dir] - start[dir] < 0;
  626. extractedEndPoint = {
  627. x: end.x,
  628. y: end.y
  629. };
  630. extractedEndPoint[dir] = chartObstacles[endObstacleIx][useMax ? dir + 'Max' : dir + 'Min'] + (useMax ? 1 : -1);
  631. endSegments.push({
  632. end: end,
  633. start: extractedEndPoint
  634. });
  635. end = extractedEndPoint;
  636. }
  637. // Find the path
  638. segments = clearPathTo(start, end, dirIsX);
  639. // Add the end-point segments
  640. segments = segments.concat(endSegments.reverse());
  641. return {
  642. path: pathFromSegments(segments),
  643. obstacles: segments
  644. };
  645. };
  646. fastAvoid.requiresObstacles = true;
  647. // Define the available pathfinding algorithms.
  648. // Algorithms take up to 3 arguments: starting point, ending point, and an
  649. // options object.
  650. var algorithms = {
  651. fastAvoid: fastAvoid,
  652. straight: straight,
  653. simpleConnect: simpleConnect
  654. };
  655. export default algorithms;