UNPKG

@spatial/shortest-path

Version:

turf shortest-path module

194 lines (174 loc) 7.38 kB
import bbox from '@spatial/bbox'; import booleanPointInPolygon from '@spatial/boolean-point-in-polygon'; import distance from '@spatial/distance'; import scale from '@spatial/transform-scale'; import cleanCoords from '@spatial/clean-coords'; import bboxPolygon from '@spatial/bbox-polygon'; import { getCoord, getType, getGeom } from '@spatial/invariant'; import { point, isNumber, lineString, isObject, featureCollection, feature } from '@spatial/helpers'; import { Graph, astar } from './lib/javascript-astar'; /** * Returns the shortest {@link LineString|path} from {@link Point|start} to {@link Point|end} without colliding with * any {@link Feature} in {@link FeatureCollection<Polygon>| obstacles} * * @name shortestPath * @param {Coord} start point * @param {Coord} end point * @param {Object} [options={}] optional parameters * @param {Geometry|Feature|FeatureCollection<Polygon>} [options.obstacles] areas which path cannot travel * @param {number} [options.minDistance] minimum distance between shortest path and obstacles * @param {string} [options.units='kilometers'] unit in which resolution & minimum distance will be expressed in; it can be degrees, radians, miles, kilometers, ... * @param {number} [options.resolution=100] distance between matrix points on which the path will be calculated * @returns {Feature<LineString>} shortest path between start and end * @example * var start = [-5, -6]; * var end = [9, -6]; * var options = { * obstacles: turf.polygon([[[0, -7], [5, -7], [5, -3], [0, -3], [0, -7]]]) * }; * * var path = turf.shortestPath(start, end, options); * * //addToMap * var addToMap = [start, end, options.obstacles, path]; */ function shortestPath(start, end, options) { // Optional parameters options = options || {}; if (!isObject(options)) throw new Error('options is invalid'); let resolution = options.resolution; const minDistance = options.minDistance; let obstacles = options.obstacles || featureCollection([]); // validation if (!start) throw new Error('start is required'); if (!end) throw new Error('end is required'); if (resolution && !isNumber(resolution) || resolution <= 0) throw new Error('options.resolution must be a number, greater than 0'); if (minDistance) throw new Error('options.minDistance is not yet implemented'); // Normalize Inputs const startCoord = getCoord(start); const endCoord = getCoord(end); start = point(startCoord); end = point(endCoord); // Handle obstacles switch (getType(obstacles)) { case 'FeatureCollection': if (obstacles.features.length === 0) return lineString([startCoord, endCoord]); break; case 'Polygon': obstacles = featureCollection([feature(getGeom(obstacles))]); break; default: throw new Error('invalid obstacles'); } // define path grid area const collection = obstacles; collection.features.push(start); collection.features.push(end); const box = bbox(scale(bboxPolygon(bbox(collection)), 1.15)); // extend 15% if (!resolution) { const width = distance([box[0], box[1]], [box[2], box[1]], options); resolution = width / 100; } collection.features.pop(); collection.features.pop(); const west = box[0]; const south = box[1]; const east = box[2]; const north = box[3]; const xFraction = resolution / (distance([west, south], [east, south], options)); const cellWidth = xFraction * (east - west); const yFraction = resolution / (distance([west, south], [west, north], options)); const cellHeight = yFraction * (north - south); const bboxHorizontalSide = (east - west); const bboxVerticalSide = (north - south); const columns = Math.floor(bboxHorizontalSide / cellWidth); const rows = Math.floor(bboxVerticalSide / cellHeight); // adjust origin of the grid const deltaX = (bboxHorizontalSide - columns * cellWidth) / 2; const deltaY = (bboxVerticalSide - rows * cellHeight) / 2; // loop through points only once to speed up process // define matrix grid for A-star algorithm const pointMatrix = []; const matrix = []; let closestToStart = []; let closestToEnd = []; let minDistStart = Infinity; let minDistEnd = Infinity; let currentY = north - deltaY; let r = 0; while (currentY >= south) { // var currentY = south + deltaY; const matrixRow = []; const pointMatrixRow = []; let currentX = west + deltaX; let c = 0; while (currentX <= east) { const pt = point([currentX, currentY]); const isInsideObstacle = isInside(pt, obstacles); // feed obstacles matrix matrixRow.push(isInsideObstacle ? 0 : 1); // with javascript-astar // matrixRow.push(isInsideObstacle ? 1 : 0); // with astar-andrea // map point's coords pointMatrixRow.push(`${currentX }|${ currentY}`); // set closest points const distStart = distance(pt, start); // if (distStart < minDistStart) { if (!isInsideObstacle && distStart < minDistStart) { minDistStart = distStart; closestToStart = {x: c, y: r}; } const distEnd = distance(pt, end); // if (distEnd < minDistEnd) { if (!isInsideObstacle && distEnd < minDistEnd) { minDistEnd = distEnd; closestToEnd = {x: c, y: r}; } currentX += cellWidth; c++; } matrix.push(matrixRow); pointMatrix.push(pointMatrixRow); currentY -= cellHeight; r++; } // find path on matrix grid // javascript-astar ---------------------- const graph = new Graph(matrix, {diagonal: true}); const startOnMatrix = graph.grid[closestToStart.y][closestToStart.x]; const endOnMatrix = graph.grid[closestToEnd.y][closestToEnd.x]; const result = astar.search(graph, startOnMatrix, endOnMatrix); const path = [startCoord]; result.forEach((coord) => { const coords = pointMatrix[coord.x][coord.y].split('|'); path.push([+coords[0], +coords[1]]); // make sure coords are numbers }); path.push(endCoord); // --------------------------------------- // astar-andrea ------------------------ // var result = aStar(matrix, [closestToStart.x, closestToStart.y], [closestToEnd.x, closestToEnd.y], 'DiagonalFree'); // var path = [start.geometry.coordinates]; // result.forEach(function (coord) { // var coords = pointMatrix[coord[1]][coord[0]].split('|'); // path.push([+coords[0], +coords[1]]); // make sure coords are numbers // }); // path.push(end.geometry.coordinates); // --------------------------------------- return cleanCoords(lineString(path)); } /** * Checks if Point is inside any of the Polygons * * @private * @param {Feature<Point>} pt to check * @param {FeatureCollection<Polygon>} polygons features * @returns {boolean} if inside or not */ function isInside(pt, polygons) { for (let i = 0; i < polygons.features.length; i++) { if (booleanPointInPolygon(pt, polygons.features[i])) { return true; } } return false; } export default shortestPath;