657 lines
27 KiB
JavaScript
657 lines
27 KiB
JavaScript
|
/* *
|
||
|
*
|
||
|
* (c) 2016 Highsoft AS
|
||
|
* Author: Øystein Moseng
|
||
|
*
|
||
|
* License: www.highcharts.com/license
|
||
|
*
|
||
|
* !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
|
||
|
*
|
||
|
* */
|
||
|
'use strict';
|
||
|
import U from '../parts/Utilities.js';
|
||
|
var extend = U.extend, pick = U.pick;
|
||
|
var min = Math.min, max = Math.max, abs = Math.abs;
|
||
|
/**
|
||
|
* Get index of last obstacle before xMin. Employs a type of binary search, and
|
||
|
* thus requires that obstacles are sorted by xMin value.
|
||
|
*
|
||
|
* @private
|
||
|
* @function findLastObstacleBefore
|
||
|
*
|
||
|
* @param {Array<object>} obstacles
|
||
|
* Array of obstacles to search in.
|
||
|
*
|
||
|
* @param {number} xMin
|
||
|
* The xMin threshold.
|
||
|
*
|
||
|
* @param {number} [startIx]
|
||
|
* Starting index to search from. Must be within array range.
|
||
|
*
|
||
|
* @return {number}
|
||
|
* The index of the last obstacle element before xMin.
|
||
|
*/
|
||
|
function findLastObstacleBefore(obstacles, xMin, startIx) {
|
||
|
var left = startIx || 0, // left limit
|
||
|
right = obstacles.length - 1, // right limit
|
||
|
min = xMin - 0.0000001, // Make sure we include all obstacles at xMin
|
||
|
cursor, cmp;
|
||
|
while (left <= right) {
|
||
|
cursor = (right + left) >> 1;
|
||
|
cmp = min - obstacles[cursor].xMin;
|
||
|
if (cmp > 0) {
|
||
|
left = cursor + 1;
|
||
|
}
|
||
|
else if (cmp < 0) {
|
||
|
right = cursor - 1;
|
||
|
}
|
||
|
else {
|
||
|
return cursor;
|
||
|
}
|
||
|
}
|
||
|
return left > 0 ? left - 1 : 0;
|
||
|
}
|
||
|
/**
|
||
|
* Test if a point lays within an obstacle.
|
||
|
*
|
||
|
* @private
|
||
|
* @function pointWithinObstacle
|
||
|
*
|
||
|
* @param {object} obstacle
|
||
|
* Obstacle to test.
|
||
|
*
|
||
|
* @param {Highcharts.Point} point
|
||
|
* Point with x/y props.
|
||
|
*
|
||
|
* @return {boolean}
|
||
|
* Whether point is within the obstacle or not.
|
||
|
*/
|
||
|
function pointWithinObstacle(obstacle, point) {
|
||
|
return (point.x <= obstacle.xMax &&
|
||
|
point.x >= obstacle.xMin &&
|
||
|
point.y <= obstacle.yMax &&
|
||
|
point.y >= obstacle.yMin);
|
||
|
}
|
||
|
/**
|
||
|
* Find the index of an obstacle that wraps around a point.
|
||
|
* Returns -1 if not found.
|
||
|
*
|
||
|
* @private
|
||
|
* @function findObstacleFromPoint
|
||
|
*
|
||
|
* @param {Array<object>} obstacles
|
||
|
* Obstacles to test.
|
||
|
*
|
||
|
* @param {Highcharts.Point} point
|
||
|
* Point with x/y props.
|
||
|
*
|
||
|
* @return {number}
|
||
|
* Ix of the obstacle in the array, or -1 if not found.
|
||
|
*/
|
||
|
function findObstacleFromPoint(obstacles, point) {
|
||
|
var i = findLastObstacleBefore(obstacles, point.x + 1) + 1;
|
||
|
while (i--) {
|
||
|
if (obstacles[i].xMax >= point.x &&
|
||
|
// optimization using lazy evaluation
|
||
|
pointWithinObstacle(obstacles[i], point)) {
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
return -1;
|
||
|
}
|
||
|
/**
|
||
|
* Get SVG path array from array of line segments.
|
||
|
*
|
||
|
* @private
|
||
|
* @function pathFromSegments
|
||
|
*
|
||
|
* @param {Array<object>} segments
|
||
|
* The segments to build the path from.
|
||
|
*
|
||
|
* @return {Highcharts.SVGPathArray}
|
||
|
* SVG path array as accepted by the SVG Renderer.
|
||
|
*/
|
||
|
function pathFromSegments(segments) {
|
||
|
var path = [];
|
||
|
if (segments.length) {
|
||
|
path.push(['M', segments[0].start.x, segments[0].start.y]);
|
||
|
for (var i = 0; i < segments.length; ++i) {
|
||
|
path.push(['L', segments[i].end.x, segments[i].end.y]);
|
||
|
}
|
||
|
}
|
||
|
return path;
|
||
|
}
|
||
|
/**
|
||
|
* Limits obstacle max/mins in all directions to bounds. Modifies input
|
||
|
* obstacle.
|
||
|
*
|
||
|
* @private
|
||
|
* @function limitObstacleToBounds
|
||
|
*
|
||
|
* @param {object} obstacle
|
||
|
* Obstacle to limit.
|
||
|
*
|
||
|
* @param {object} bounds
|
||
|
* Bounds to use as limit.
|
||
|
*
|
||
|
* @return {void}
|
||
|
*/
|
||
|
function limitObstacleToBounds(obstacle, bounds) {
|
||
|
obstacle.yMin = max(obstacle.yMin, bounds.yMin);
|
||
|
obstacle.yMax = min(obstacle.yMax, bounds.yMax);
|
||
|
obstacle.xMin = max(obstacle.xMin, bounds.xMin);
|
||
|
obstacle.xMax = min(obstacle.xMax, bounds.xMax);
|
||
|
}
|
||
|
// Define the available pathfinding algorithms.
|
||
|
// Algorithms take up to 3 arguments: starting point, ending point, and an
|
||
|
// options object.
|
||
|
var algorithms = {
|
||
|
/**
|
||
|
* Get an SVG path from a starting coordinate to an ending coordinate.
|
||
|
* Draws a straight line.
|
||
|
*
|
||
|
* @function Highcharts.Pathfinder.algorithms.straight
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} start
|
||
|
* Starting coordinate, object with x/y props.
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} end
|
||
|
* Ending coordinate, object with x/y props.
|
||
|
*
|
||
|
* @return {object}
|
||
|
* An object with the SVG path in Array form as accepted by the SVG
|
||
|
* renderer, as well as an array of new obstacles making up this
|
||
|
* path.
|
||
|
*/
|
||
|
straight: function (start, end) {
|
||
|
return {
|
||
|
path: [
|
||
|
['M', start.x, start.y],
|
||
|
['L', end.x, end.y]
|
||
|
],
|
||
|
obstacles: [{ start: start, end: end }]
|
||
|
};
|
||
|
},
|
||
|
/**
|
||
|
* Find a path from a starting coordinate to an ending coordinate, using
|
||
|
* right angles only, and taking only starting/ending obstacle into
|
||
|
* consideration.
|
||
|
*
|
||
|
* @function Highcharts.Pathfinder.algorithms.simpleConnect
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} start
|
||
|
* Starting coordinate, object with x/y props.
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} end
|
||
|
* Ending coordinate, object with x/y props.
|
||
|
*
|
||
|
* @param {object} options
|
||
|
* Options for the algorithm:
|
||
|
* - chartObstacles: Array of chart obstacles to avoid
|
||
|
* - startDirectionX: Optional. True if starting in the X direction.
|
||
|
* If not provided, the algorithm starts in the direction that is
|
||
|
* the furthest between start/end.
|
||
|
*
|
||
|
* @return {object}
|
||
|
* An object with the SVG path in Array form as accepted by the SVG
|
||
|
* renderer, as well as an array of new obstacles making up this
|
||
|
* path.
|
||
|
*/
|
||
|
simpleConnect: extend(function (start, end, options) {
|
||
|
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;
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* Return a clone of a point with a property set from a target object,
|
||
|
* optionally with an offset
|
||
|
* @private
|
||
|
*/
|
||
|
function copyFromPoint(from, fromKey, to, toKey, offset) {
|
||
|
var point = {
|
||
|
x: from.x,
|
||
|
y: from.y
|
||
|
};
|
||
|
point[fromKey] = to[toKey || fromKey] + (offset || 0);
|
||
|
return point;
|
||
|
}
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* Return waypoint outside obstacle.
|
||
|
* @private
|
||
|
*/
|
||
|
function getMeOut(obstacle, point, direction) {
|
||
|
var useMax = abs(point[direction] - obstacle[direction + 'Min']) >
|
||
|
abs(point[direction] - obstacle[direction + 'Max']);
|
||
|
return copyFromPoint(point, direction, obstacle, direction + (useMax ? 'Max' : 'Min'), useMax ? 1 : -1);
|
||
|
}
|
||
|
// Pull out end point
|
||
|
if (endObstacleIx > -1) {
|
||
|
endObstacle = chartObstacles[endObstacleIx];
|
||
|
waypoint = getMeOut(endObstacle, end, dir);
|
||
|
endSegment = {
|
||
|
start: waypoint,
|
||
|
end: end
|
||
|
};
|
||
|
endPoint = waypoint;
|
||
|
}
|
||
|
else {
|
||
|
endPoint = end;
|
||
|
}
|
||
|
// If an obstacle envelops the start point, add a segment to get out,
|
||
|
// and around it.
|
||
|
if (startObstacleIx > -1) {
|
||
|
startObstacle = chartObstacles[startObstacleIx];
|
||
|
waypoint = getMeOut(startObstacle, start, dir);
|
||
|
segments.push({
|
||
|
start: start,
|
||
|
end: waypoint
|
||
|
});
|
||
|
// If we are going back again, switch direction to get around start
|
||
|
// obstacle.
|
||
|
if (
|
||
|
// Going towards max from start:
|
||
|
waypoint[dir] >= start[dir] ===
|
||
|
// Going towards min to end:
|
||
|
waypoint[dir] >= endPoint[dir]) {
|
||
|
dir = dir === 'y' ? 'x' : 'y';
|
||
|
useMax = start[dir] < end[dir];
|
||
|
segments.push({
|
||
|
start: waypoint,
|
||
|
end: copyFromPoint(waypoint, dir, startObstacle, dir + (useMax ? 'Max' : 'Min'), useMax ? 1 : -1)
|
||
|
});
|
||
|
// Switch direction again
|
||
|
dir = dir === 'y' ? 'x' : 'y';
|
||
|
}
|
||
|
}
|
||
|
// We are around the start obstacle. Go towards the end in one
|
||
|
// direction.
|
||
|
prevWaypoint = segments.length ?
|
||
|
segments[segments.length - 1].end :
|
||
|
start;
|
||
|
waypoint = copyFromPoint(prevWaypoint, dir, endPoint);
|
||
|
segments.push({
|
||
|
start: prevWaypoint,
|
||
|
end: waypoint
|
||
|
});
|
||
|
// Final run to end point in the other direction
|
||
|
dir = dir === 'y' ? 'x' : 'y';
|
||
|
waypoint2 = copyFromPoint(waypoint, dir, endPoint);
|
||
|
segments.push({
|
||
|
start: waypoint,
|
||
|
end: waypoint2
|
||
|
});
|
||
|
// Finally add the endSegment
|
||
|
segments.push(endSegment);
|
||
|
return {
|
||
|
path: pathFromSegments(segments),
|
||
|
obstacles: segments
|
||
|
};
|
||
|
}, {
|
||
|
requiresObstacles: true
|
||
|
}),
|
||
|
/**
|
||
|
* Find a path from a starting coordinate to an ending coordinate, taking
|
||
|
* obstacles into consideration. Might not always find the optimal path,
|
||
|
* but is fast, and usually good enough.
|
||
|
*
|
||
|
* @function Highcharts.Pathfinder.algorithms.fastAvoid
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} start
|
||
|
* Starting coordinate, object with x/y props.
|
||
|
*
|
||
|
* @param {Highcharts.PositionObject} end
|
||
|
* Ending coordinate, object with x/y props.
|
||
|
*
|
||
|
* @param {object} options
|
||
|
* Options for the algorithm.
|
||
|
* - chartObstacles: Array of chart obstacles to avoid
|
||
|
* - lineObstacles: Array of line obstacles to jump over
|
||
|
* - obstacleMetrics: Object with metrics of chartObstacles cached
|
||
|
* - hardBounds: Hard boundaries to not cross
|
||
|
* - obstacleOptions: Options for the obstacles, including margin
|
||
|
* - startDirectionX: Optional. True if starting in the X direction.
|
||
|
* If not provided, the algorithm starts in the
|
||
|
* direction that is the furthest between
|
||
|
* start/end.
|
||
|
*
|
||
|
* @return {object}
|
||
|
* An object with the SVG path in Array form as accepted by the SVG
|
||
|
* renderer, as well as an array of new obstacles making up this
|
||
|
* path.
|
||
|
*/
|
||
|
fastAvoid: extend(function (start, end, options) {
|
||
|
/*
|
||
|
Algorithm rules/description
|
||
|
- Find initial direction
|
||
|
- Determine soft/hard max for each direction.
|
||
|
- Move along initial direction until obstacle.
|
||
|
- Change direction.
|
||
|
- If hitting obstacle, first try to change length of previous line
|
||
|
before changing direction again.
|
||
|
|
||
|
Soft min/max x = start/destination x +/- widest obstacle + margin
|
||
|
Soft min/max y = start/destination y +/- tallest obstacle + margin
|
||
|
|
||
|
@todo:
|
||
|
- Make retrospective, try changing prev segment to reduce
|
||
|
corners
|
||
|
- Fix logic for breaking out of end-points - not always picking
|
||
|
the best direction currently
|
||
|
- When going around the end obstacle we should not always go the
|
||
|
shortest route, rather pick the one closer to the end point
|
||
|
*/
|
||
|
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
|
||
|
// when to force break through an obstacle.
|
||
|
// Boundaries to stay within. If beyond soft boundary, prefer to
|
||
|
// change direction ASAP. If at hard max, always change immediately.
|
||
|
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,
|
||
|
// Obstacles
|
||
|
chartObstacles = options.chartObstacles, startObstacleIx = findLastObstacleBefore(chartObstacles, softMinX), endObstacleIx = findLastObstacleBefore(chartObstacles, softMaxX);
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* How far can you go between two points before hitting an obstacle?
|
||
|
* Does not work for diagonal lines (because it doesn't have to).
|
||
|
* @private
|
||
|
*/
|
||
|
function pivotPoint(fromPoint, toPoint, directionIsX) {
|
||
|
var firstPoint, lastPoint, highestPoint, lowestPoint, i, searchDirection = fromPoint.x < toPoint.x ? 1 : -1;
|
||
|
if (fromPoint.x < toPoint.x) {
|
||
|
firstPoint = fromPoint;
|
||
|
lastPoint = toPoint;
|
||
|
}
|
||
|
else {
|
||
|
firstPoint = toPoint;
|
||
|
lastPoint = fromPoint;
|
||
|
}
|
||
|
if (fromPoint.y < toPoint.y) {
|
||
|
lowestPoint = fromPoint;
|
||
|
highestPoint = toPoint;
|
||
|
}
|
||
|
else {
|
||
|
lowestPoint = toPoint;
|
||
|
highestPoint = fromPoint;
|
||
|
}
|
||
|
// Go through obstacle range in reverse if toPoint is before
|
||
|
// fromPoint in the X-dimension.
|
||
|
i = searchDirection < 0 ?
|
||
|
// Searching backwards, start at last obstacle before last point
|
||
|
min(findLastObstacleBefore(chartObstacles, lastPoint.x), chartObstacles.length - 1) :
|
||
|
// Forwards. Since we're not sorted by xMax, we have to look
|
||
|
// at all obstacles.
|
||
|
0;
|
||
|
// Go through obstacles in this X range
|
||
|
while (chartObstacles[i] && (searchDirection > 0 && chartObstacles[i].xMin <= lastPoint.x ||
|
||
|
searchDirection < 0 && chartObstacles[i].xMax >= firstPoint.x)) {
|
||
|
// If this obstacle is between from and to points in a straight
|
||
|
// line, pivot at the intersection.
|
||
|
if (chartObstacles[i].xMin <= lastPoint.x &&
|
||
|
chartObstacles[i].xMax >= firstPoint.x &&
|
||
|
chartObstacles[i].yMin <= highestPoint.y &&
|
||
|
chartObstacles[i].yMax >= lowestPoint.y) {
|
||
|
if (directionIsX) {
|
||
|
return {
|
||
|
y: fromPoint.y,
|
||
|
x: fromPoint.x < toPoint.x ?
|
||
|
chartObstacles[i].xMin - 1 :
|
||
|
chartObstacles[i].xMax + 1,
|
||
|
obstacle: chartObstacles[i]
|
||
|
};
|
||
|
}
|
||
|
// else ...
|
||
|
return {
|
||
|
x: fromPoint.x,
|
||
|
y: fromPoint.y < toPoint.y ?
|
||
|
chartObstacles[i].yMin - 1 :
|
||
|
chartObstacles[i].yMax + 1,
|
||
|
obstacle: chartObstacles[i]
|
||
|
};
|
||
|
}
|
||
|
i += searchDirection;
|
||
|
}
|
||
|
return toPoint;
|
||
|
}
|
||
|
/**
|
||
|
* Decide in which direction to dodge or get out of an obstacle.
|
||
|
* Considers desired direction, which way is shortest, soft and hard
|
||
|
* bounds.
|
||
|
*
|
||
|
* (? Returns a string, either xMin, xMax, yMin or yMax.)
|
||
|
*
|
||
|
* @private
|
||
|
* @function
|
||
|
*
|
||
|
* @param {object} obstacle
|
||
|
* Obstacle to dodge/escape.
|
||
|
*
|
||
|
* @param {object} fromPoint
|
||
|
* Point with x/y props that's dodging/escaping.
|
||
|
*
|
||
|
* @param {object} toPoint
|
||
|
* Goal point.
|
||
|
*
|
||
|
* @param {boolean} dirIsX
|
||
|
* Dodge in X dimension.
|
||
|
*
|
||
|
* @param {object} bounds
|
||
|
* Hard and soft boundaries.
|
||
|
*
|
||
|
* @return {boolean}
|
||
|
* Use max or not.
|
||
|
*/
|
||
|
function getDodgeDirection(obstacle, fromPoint, toPoint, dirIsX, bounds) {
|
||
|
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'] >=
|
||
|
softBounds[dir + 'Max'], minOutOfSoftBounds = obstacle[dir + 'Min'] <=
|
||
|
softBounds[dir + 'Min'], maxOutOfHardBounds = obstacle[dir + 'Max'] >=
|
||
|
hardBounds[dir + 'Max'], minOutOfHardBounds = obstacle[dir + 'Min'] <=
|
||
|
hardBounds[dir + 'Min'],
|
||
|
// Find out if we should prefer one direction over the other if
|
||
|
// we can choose freely
|
||
|
minDistance = abs(obstacle[dir + 'Min'] - fromPoint[dir]), maxDistance = abs(obstacle[dir + 'Max'] - fromPoint[dir]),
|
||
|
// If it's a small difference, pick the one leading towards dest
|
||
|
// point. Otherwise pick the shortest distance
|
||
|
useMax = abs(minDistance - maxDistance) < 10 ?
|
||
|
fromPoint[dir] < toPoint[dir] :
|
||
|
maxDistance < minDistance;
|
||
|
// Check if we hit any obstacles trying to go around in either
|
||
|
// direction.
|
||
|
toPointMin[dir] = obstacle[dir + 'Min'];
|
||
|
toPointMax[dir] = obstacle[dir + 'Max'];
|
||
|
minPivot = pivotPoint(fromPoint, toPointMin, dirIsX)[dir] !==
|
||
|
toPointMin[dir];
|
||
|
maxPivot = pivotPoint(fromPoint, toPointMax, dirIsX)[dir] !==
|
||
|
toPointMax[dir];
|
||
|
useMax = minPivot ?
|
||
|
(maxPivot ? useMax : true) :
|
||
|
(maxPivot ? false : useMax);
|
||
|
// useMax now contains our preferred choice, bounds not taken into
|
||
|
// account. If both or neither direction is out of bounds we want to
|
||
|
// use this.
|
||
|
// Deal with soft bounds
|
||
|
useMax = minOutOfSoftBounds ?
|
||
|
(maxOutOfSoftBounds ? useMax : true) : // Out on min
|
||
|
(maxOutOfSoftBounds ? false : useMax); // Not out on min
|
||
|
// Deal with hard bounds
|
||
|
useMax = minOutOfHardBounds ?
|
||
|
(maxOutOfHardBounds ? useMax : true) : // Out on min
|
||
|
(maxOutOfHardBounds ? false : useMax); // Not out on min
|
||
|
return useMax;
|
||
|
}
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* Find a clear path between point.
|
||
|
* @private
|
||
|
*/
|
||
|
function clearPathTo(fromPoint, toPoint, dirIsX) {
|
||
|
// Don't waste time if we've hit goal
|
||
|
if (fromPoint.x === toPoint.x && fromPoint.y === toPoint.y) {
|
||
|
return [];
|
||
|
}
|
||
|
var dir = dirIsX ? 'x' : 'y', pivot, segments, waypoint, waypointUseMax, envelopingObstacle, secondEnvelopingObstacle, envelopWaypoint, obstacleMargin = options.obstacleOptions.margin, bounds = {
|
||
|
soft: {
|
||
|
xMin: softMinX,
|
||
|
xMax: softMaxX,
|
||
|
yMin: softMinY,
|
||
|
yMax: softMaxY
|
||
|
},
|
||
|
hard: options.hardBounds
|
||
|
};
|
||
|
// If fromPoint is inside an obstacle we have a problem. Break out
|
||
|
// by just going to the outside of this obstacle. We prefer to go to
|
||
|
// the nearest edge in the chosen direction.
|
||
|
envelopingObstacle =
|
||
|
findObstacleFromPoint(chartObstacles, fromPoint);
|
||
|
if (envelopingObstacle > -1) {
|
||
|
envelopingObstacle = chartObstacles[envelopingObstacle];
|
||
|
waypointUseMax = getDodgeDirection(envelopingObstacle, fromPoint, toPoint, dirIsX, bounds);
|
||
|
// Cut obstacle to hard bounds to make sure we stay within
|
||
|
limitObstacleToBounds(envelopingObstacle, options.hardBounds);
|
||
|
envelopWaypoint = dirIsX ? {
|
||
|
y: fromPoint.y,
|
||
|
x: envelopingObstacle[waypointUseMax ? 'xMax' : 'xMin'] +
|
||
|
(waypointUseMax ? 1 : -1)
|
||
|
} : {
|
||
|
x: fromPoint.x,
|
||
|
y: envelopingObstacle[waypointUseMax ? 'yMax' : 'yMin'] +
|
||
|
(waypointUseMax ? 1 : -1)
|
||
|
};
|
||
|
// If we crashed into another obstacle doing this, we put the
|
||
|
// waypoint between them instead
|
||
|
secondEnvelopingObstacle = findObstacleFromPoint(chartObstacles, envelopWaypoint);
|
||
|
if (secondEnvelopingObstacle > -1) {
|
||
|
secondEnvelopingObstacle = chartObstacles[secondEnvelopingObstacle];
|
||
|
// Cut obstacle to hard bounds
|
||
|
limitObstacleToBounds(secondEnvelopingObstacle, options.hardBounds);
|
||
|
// Modify waypoint to lay between obstacles
|
||
|
envelopWaypoint[dir] = waypointUseMax ? max(envelopingObstacle[dir + 'Max'] - obstacleMargin + 1, (secondEnvelopingObstacle[dir + 'Min'] +
|
||
|
envelopingObstacle[dir + 'Max']) / 2) :
|
||
|
min((envelopingObstacle[dir + 'Min'] + obstacleMargin - 1), ((secondEnvelopingObstacle[dir + 'Max'] +
|
||
|
envelopingObstacle[dir + 'Min']) / 2));
|
||
|
// We are not going anywhere. If this happens for the first
|
||
|
// time, do nothing. Otherwise, try to go to the extreme of
|
||
|
// the obstacle pair in the current direction.
|
||
|
if (fromPoint.x === envelopWaypoint.x &&
|
||
|
fromPoint.y === envelopWaypoint.y) {
|
||
|
if (forceObstacleBreak) {
|
||
|
envelopWaypoint[dir] = waypointUseMax ?
|
||
|
max(envelopingObstacle[dir + 'Max'], secondEnvelopingObstacle[dir + 'Max']) + 1 :
|
||
|
min(envelopingObstacle[dir + 'Min'], secondEnvelopingObstacle[dir + 'Min']) - 1;
|
||
|
}
|
||
|
// Toggle on if off, and the opposite
|
||
|
forceObstacleBreak = !forceObstacleBreak;
|
||
|
}
|
||
|
else {
|
||
|
// This point is not identical to previous.
|
||
|
// Clear break trigger.
|
||
|
forceObstacleBreak = false;
|
||
|
}
|
||
|
}
|
||
|
segments = [{
|
||
|
start: fromPoint,
|
||
|
end: envelopWaypoint
|
||
|
}];
|
||
|
}
|
||
|
else { // If not enveloping, use standard pivot calculation
|
||
|
pivot = pivotPoint(fromPoint, {
|
||
|
x: dirIsX ? toPoint.x : fromPoint.x,
|
||
|
y: dirIsX ? fromPoint.y : toPoint.y
|
||
|
}, dirIsX);
|
||
|
segments = [{
|
||
|
start: fromPoint,
|
||
|
end: {
|
||
|
x: pivot.x,
|
||
|
y: pivot.y
|
||
|
}
|
||
|
}];
|
||
|
// Pivot before goal, use a waypoint to dodge obstacle
|
||
|
if (pivot[dirIsX ? 'x' : 'y'] !== toPoint[dirIsX ? 'x' : 'y']) {
|
||
|
// Find direction of waypoint
|
||
|
waypointUseMax = getDodgeDirection(pivot.obstacle, pivot, toPoint, !dirIsX, bounds);
|
||
|
// Cut waypoint to hard bounds
|
||
|
limitObstacleToBounds(pivot.obstacle, options.hardBounds);
|
||
|
waypoint = {
|
||
|
x: dirIsX ?
|
||
|
pivot.x :
|
||
|
pivot.obstacle[waypointUseMax ? 'xMax' : 'xMin'] +
|
||
|
(waypointUseMax ? 1 : -1),
|
||
|
y: dirIsX ?
|
||
|
pivot.obstacle[waypointUseMax ? 'yMax' : 'yMin'] +
|
||
|
(waypointUseMax ? 1 : -1) :
|
||
|
pivot.y
|
||
|
};
|
||
|
// We're changing direction here, store that to make sure we
|
||
|
// also change direction when adding the last segment array
|
||
|
// after handling waypoint.
|
||
|
dirIsX = !dirIsX;
|
||
|
segments = segments.concat(clearPathTo({
|
||
|
x: pivot.x,
|
||
|
y: pivot.y
|
||
|
}, waypoint, dirIsX));
|
||
|
}
|
||
|
}
|
||
|
// Get segments for the other direction too
|
||
|
// Recursion is our friend
|
||
|
segments = segments.concat(clearPathTo(segments[segments.length - 1].end, toPoint, !dirIsX));
|
||
|
return segments;
|
||
|
}
|
||
|
// eslint-disable-next-line valid-jsdoc
|
||
|
/**
|
||
|
* Extract point to outside of obstacle in whichever direction is
|
||
|
* closest. Returns new point outside obstacle.
|
||
|
* @private
|
||
|
*/
|
||
|
function extractFromObstacle(obstacle, point, goalPoint) {
|
||
|
var dirIsX = min(obstacle.xMax - point.x, point.x - obstacle.xMin) <
|
||
|
min(obstacle.yMax - point.y, point.y - obstacle.yMin), bounds = {
|
||
|
soft: options.hardBounds,
|
||
|
hard: options.hardBounds
|
||
|
}, useMax = getDodgeDirection(obstacle, point, goalPoint, dirIsX, bounds);
|
||
|
return dirIsX ? {
|
||
|
y: point.y,
|
||
|
x: obstacle[useMax ? 'xMax' : 'xMin'] + (useMax ? 1 : -1)
|
||
|
} : {
|
||
|
x: point.x,
|
||
|
y: obstacle[useMax ? 'yMax' : 'yMin'] + (useMax ? 1 : -1)
|
||
|
};
|
||
|
}
|
||
|
// Cut the obstacle array to soft bounds for optimization in large
|
||
|
// datasets.
|
||
|
chartObstacles =
|
||
|
chartObstacles.slice(startObstacleIx, endObstacleIx + 1);
|
||
|
// If an obstacle envelops the end point, move it out of there and add
|
||
|
// a little segment to where it was.
|
||
|
if ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
|
||
|
extractedEndPoint = extractFromObstacle(chartObstacles[endObstacleIx], end, start);
|
||
|
endSegments.push({
|
||
|
end: end,
|
||
|
start: extractedEndPoint
|
||
|
});
|
||
|
end = extractedEndPoint;
|
||
|
}
|
||
|
// If it's still inside one or more obstacles, get out of there by
|
||
|
// force-moving towards the start point.
|
||
|
while ((endObstacleIx = findObstacleFromPoint(chartObstacles, end)) > -1) {
|
||
|
useMax = end[dir] - start[dir] < 0;
|
||
|
extractedEndPoint = {
|
||
|
x: end.x,
|
||
|
y: end.y
|
||
|
};
|
||
|
extractedEndPoint[dir] = chartObstacles[endObstacleIx][useMax ? dir + 'Max' : dir + 'Min'] + (useMax ? 1 : -1);
|
||
|
endSegments.push({
|
||
|
end: end,
|
||
|
start: extractedEndPoint
|
||
|
});
|
||
|
end = extractedEndPoint;
|
||
|
}
|
||
|
// Find the path
|
||
|
segments = clearPathTo(start, end, dirIsX);
|
||
|
// Add the end-point segments
|
||
|
segments = segments.concat(endSegments.reverse());
|
||
|
return {
|
||
|
path: pathFromSegments(segments),
|
||
|
obstacles: segments
|
||
|
};
|
||
|
}, {
|
||
|
requiresObstacles: true
|
||
|
})
|
||
|
};
|
||
|
export default algorithms;
|