PathfinderAlgorithms.js 27 KB

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