PathfinderAlgorithms.js 29 KB

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