|
| 1 | +interface Point { |
| 2 | + x: number; |
| 3 | + y: number; |
| 4 | +} |
| 5 | + |
| 6 | +interface Adjacent { |
| 7 | + point: Point; |
| 8 | + edgeWeight: number; |
| 9 | +} |
| 10 | + |
| 11 | +interface GraphNode { |
| 12 | + point: Point; |
| 13 | + adjacent: Adjacent[]; |
| 14 | + dist: number; |
| 15 | + path: Point[]; |
| 16 | +} |
| 17 | + |
| 18 | +function distance(a: Point, b: Point): number { |
| 19 | + return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); |
| 20 | +} |
| 21 | + |
| 22 | +function isChangedDirection(a: GraphNode, b: GraphNode): boolean { |
| 23 | + if (a.path.length === 0) return false; |
| 24 | + |
| 25 | + const commingX2 = a.point.x; |
| 26 | + const commingX1 = a.path[a.path.length - 1].x; |
| 27 | + const commingHorizontal = commingX2 - commingX1 ? false : true; |
| 28 | + |
| 29 | + const goingHorizontal = a.point.x - b.point.x ? false : true; |
| 30 | + |
| 31 | + return commingHorizontal !== goingHorizontal; |
| 32 | +} |
| 33 | + |
| 34 | +function filterUnchangedDirection(path: Point[]): Point[] { |
| 35 | + return path.filter((p, i, v) => { |
| 36 | + if (i === 0 || i === v.length - 1) return true; |
| 37 | + |
| 38 | + const commingDirection = |
| 39 | + v[i].x - v[i - 1].x !== 0 ? 'horizontal' : 'vertical'; |
| 40 | + const goingDirection = |
| 41 | + v[i + 1].x - v[i].x !== 0 ? 'horizontal' : 'vertical'; |
| 42 | + |
| 43 | + return commingDirection === goingDirection ? false : true; |
| 44 | + }); |
| 45 | +} |
| 46 | + |
| 47 | +function calculateMinimumDistance( |
| 48 | + adjacent: GraphNode, |
| 49 | + edgeWeigh: number, |
| 50 | + parentNode: GraphNode |
| 51 | +) { |
| 52 | + const sourceDistance = parentNode.dist; |
| 53 | + |
| 54 | + const changingDirection = isChangedDirection(parentNode, adjacent); |
| 55 | + const extraWeigh = changingDirection ? Math.pow(edgeWeigh + 1, 2) : 0; |
| 56 | + |
| 57 | + if (sourceDistance + edgeWeigh + extraWeigh < adjacent.dist) { |
| 58 | + adjacent.dist = sourceDistance + edgeWeigh + extraWeigh; |
| 59 | + const shortestPath: Point[] = [...parentNode.path]; |
| 60 | + shortestPath.push(parentNode.point); |
| 61 | + adjacent.path = shortestPath; |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +function getLowestDistanceGraphNode( |
| 66 | + unsettledNodes: Set<GraphNode> |
| 67 | +): GraphNode | null { |
| 68 | + let lowestDistance = Number.MAX_SAFE_INTEGER; |
| 69 | + let lowestDistanceNode: GraphNode | null = null; |
| 70 | + |
| 71 | + for (const node of unsettledNodes) |
| 72 | + if (node.dist < lowestDistance) { |
| 73 | + lowestDistance = node.dist; |
| 74 | + lowestDistanceNode = node; |
| 75 | + } |
| 76 | + |
| 77 | + return lowestDistanceNode; |
| 78 | +} |
| 79 | + |
| 80 | +function dijkstra(graph: GraphNode[], start: GraphNode): Point[] { |
| 81 | + start.dist = 0; |
| 82 | + |
| 83 | + const settledNodes: Set<GraphNode> = new Set(); |
| 84 | + const unsettledNodes: Set<GraphNode> = new Set(); |
| 85 | + |
| 86 | + unsettledNodes.add(start); |
| 87 | + |
| 88 | + while (unsettledNodes.size != 0) { |
| 89 | + const currentNode = getLowestDistanceGraphNode(unsettledNodes)!; |
| 90 | + unsettledNodes.delete(currentNode); |
| 91 | + |
| 92 | + for (const adjacent of currentNode.adjacent) { |
| 93 | + const adjacentNode = graph.find( |
| 94 | + node => |
| 95 | + node.point.x === adjacent.point.x && node.point.y === adjacent.point.y |
| 96 | + ); |
| 97 | + const edgeWeight = adjacent.edgeWeight; |
| 98 | + if (adjacentNode && !settledNodes.has(adjacentNode)) { |
| 99 | + calculateMinimumDistance(adjacentNode, edgeWeight, currentNode); |
| 100 | + unsettledNodes.add(adjacentNode); |
| 101 | + } |
| 102 | + } |
| 103 | + settledNodes.add(currentNode); |
| 104 | + } |
| 105 | + |
| 106 | + return []; |
| 107 | +} |
| 108 | + |
| 109 | +function findClosestGraphNode( |
| 110 | + graph: GraphNode[], |
| 111 | + point: Point |
| 112 | +): GraphNode | undefined { |
| 113 | + const distFromGraphNodes = graph.map( |
| 114 | + node => Math.abs(point.x - node.point.x) + Math.abs(point.y - node.point.y) |
| 115 | + ); |
| 116 | + |
| 117 | + const minDistance = Math.min(...distFromGraphNodes); |
| 118 | + const index = distFromGraphNodes.indexOf(minDistance); |
| 119 | + |
| 120 | + return graph[index]; |
| 121 | +} |
| 122 | + |
| 123 | +function addStartNode(graph: GraphNode[], start: Point): GraphNode | undefined { |
| 124 | + const closestToStart = findClosestGraphNode(graph, start)?.point; |
| 125 | + if (!closestToStart) return undefined; |
| 126 | + |
| 127 | + const startNode = { |
| 128 | + point: start, |
| 129 | + adjacent: [ |
| 130 | + { point: closestToStart, edgeWeight: distance(start, closestToStart) }, |
| 131 | + ], |
| 132 | + dist: Number.MAX_SAFE_INTEGER, |
| 133 | + path: [], |
| 134 | + }; |
| 135 | + graph.push(startNode); |
| 136 | + |
| 137 | + return startNode; |
| 138 | +} |
| 139 | + |
| 140 | +function getPath(graph: GraphNode[], start: Point, end: Point): Point[] { |
| 141 | + const startNode = addStartNode(graph, start); |
| 142 | + const closestToEnd = findClosestGraphNode(graph, end); |
| 143 | + |
| 144 | + if (!startNode || !closestToEnd) return []; |
| 145 | + |
| 146 | + dijkstra(graph, startNode); |
| 147 | + const shortestPath = closestToEnd.path.concat(closestToEnd.point); |
| 148 | + |
| 149 | + return filterUnchangedDirection(shortestPath).concat([end]); |
| 150 | +} |
| 151 | + |
| 152 | +function findGraphNode( |
| 153 | + graph: GraphNode[], |
| 154 | + x: number, |
| 155 | + y: number |
| 156 | +): GraphNode | undefined { |
| 157 | + return graph.find(node => node.point.x === x && node.point.y === y); |
| 158 | +} |
| 159 | + |
| 160 | +function findAdjacent( |
| 161 | + graph: GraphNode[], |
| 162 | + currentNode: GraphNode, |
| 163 | + gridSize: number, |
| 164 | + type: string |
| 165 | +): Adjacent | null { |
| 166 | + let dX1: number; |
| 167 | + let dY1: number; |
| 168 | + |
| 169 | + if (type === 'prevX') { |
| 170 | + dX1 = currentNode.point.x - gridSize; |
| 171 | + dY1 = currentNode.point.y; |
| 172 | + } else if (type === 'prevY') { |
| 173 | + dX1 = currentNode.point.x; |
| 174 | + dY1 = currentNode.point.y - gridSize; |
| 175 | + } else if (type === 'nextX') { |
| 176 | + dX1 = currentNode.point.x + gridSize; |
| 177 | + dY1 = currentNode.point.y; |
| 178 | + } else { |
| 179 | + dX1 = currentNode.point.x; |
| 180 | + dY1 = currentNode.point.y + gridSize; |
| 181 | + } |
| 182 | + |
| 183 | + if (findGraphNode(graph, dX1!, dY1!)) { |
| 184 | + return { |
| 185 | + point: findGraphNode(graph, dX1!, dY1!)!.point, |
| 186 | + edgeWeight: gridSize, |
| 187 | + }; |
| 188 | + } |
| 189 | + |
| 190 | + return null; |
| 191 | +} |
| 192 | + |
| 193 | +function createGraph(allocation: number[][], gridSize: number): GraphNode[] { |
| 194 | + const graph: GraphNode[] = []; |
| 195 | + for (let row = 0; row < allocation.length; row++) |
| 196 | + for (let col = 0; col < allocation[row].length; col++) |
| 197 | + if (allocation[row][col] === 0) |
| 198 | + graph.push({ |
| 199 | + point: { |
| 200 | + x: col * gridSize + gridSize / 2, |
| 201 | + y: row * gridSize + gridSize / 2, |
| 202 | + }, |
| 203 | + adjacent: [], |
| 204 | + dist: Number.MAX_SAFE_INTEGER, |
| 205 | + path: [], |
| 206 | + }); |
| 207 | + |
| 208 | + for (const node of graph) { |
| 209 | + const adjacents = <Adjacent[]>( |
| 210 | + ['prevX', 'prevY', 'nextX', 'nextY'] |
| 211 | + .map(type => findAdjacent(graph, node, gridSize, type)) |
| 212 | + .filter(adjacent => adjacent) |
| 213 | + ); |
| 214 | + node.adjacent = adjacents; |
| 215 | + } |
| 216 | + |
| 217 | + return graph; |
| 218 | +} |
| 219 | + |
| 220 | +function emptyAllocation( |
| 221 | + start: Point, |
| 222 | + end: Point, |
| 223 | + gridSize: number |
| 224 | +): (0 | 1)[][] { |
| 225 | + const maxX = start.x > end.x ? start.x : end.x; |
| 226 | + const maxY = start.y > end.y ? start.y : end.y; |
| 227 | + |
| 228 | + const emptyGrid: (0 | 1)[][] = []; |
| 229 | + for (let i = 0; i <= Math.ceil(maxY / gridSize) + 1; i++) { |
| 230 | + emptyGrid[i] = []; |
| 231 | + for (let j = 0; j <= Math.ceil(maxX / gridSize) + 1; j++) { |
| 232 | + emptyGrid[i][j] = 0; |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + emptyGrid[Math.floor(start.y / gridSize)][Math.floor(start.x / gridSize)] = 1; |
| 237 | + emptyGrid[Math.floor(end.y / gridSize)][Math.floor(end.x / gridSize)] = 1; |
| 238 | + |
| 239 | + return emptyGrid; |
| 240 | +} |
| 241 | + |
| 242 | +//FIXME: This is a dirty trick to improve performance of the algorithm |
| 243 | +function trimStartEnd(start: Point, end: Point, gridSize: number): Point[] { |
| 244 | + //FIXME: Dirty hack to speed up the algorithm |
| 245 | + const minCoordX = Math.min( |
| 246 | + Math.floor(start.x / gridSize), |
| 247 | + Math.floor(end.x / gridSize) |
| 248 | + ); |
| 249 | + const minCoordY = Math.min( |
| 250 | + Math.floor(start.y / gridSize), |
| 251 | + Math.floor(end.y / gridSize) |
| 252 | + ); |
| 253 | + |
| 254 | + const dCoordX = minCoordX > 1 ? minCoordX - 1 : 0; |
| 255 | + const dCoordY = minCoordY > 1 ? minCoordY - 1 : 0; |
| 256 | + |
| 257 | + const deltaX = dCoordX * gridSize; |
| 258 | + const deltaY = dCoordY * gridSize; |
| 259 | + |
| 260 | + return [ |
| 261 | + { x: start.x - deltaX, y: start.y - deltaY }, |
| 262 | + { x: end.x - deltaX, y: end.y - deltaY }, |
| 263 | + ]; |
| 264 | +} |
| 265 | + |
| 266 | +function fullPath( |
| 267 | + path: Point[], |
| 268 | + start: Point, |
| 269 | + end: Point, |
| 270 | + trimmedStart: Point, |
| 271 | + trimmedEnd: Point |
| 272 | +): Point[] { |
| 273 | + if (start === trimmedStart && end === trimmedEnd) return path; |
| 274 | + |
| 275 | + const deltaX = start.x - trimmedStart.x; |
| 276 | + const deltaY = start.y - trimmedStart.y; |
| 277 | + |
| 278 | + return path.map(point => { |
| 279 | + return { x: point.x + deltaX, y: point.y + deltaY }; |
| 280 | + }); |
| 281 | +} |
| 282 | + |
| 283 | +/** Finds the shortest orthogonal path between start and end based on grid and dijkstra path finding algorithm |
| 284 | + * @param start - the position in px of the start point |
| 285 | + * @param end - the position in px of the end point |
| 286 | + * @param gridSize - grid size of the grid to rout in the orthogonal path |
| 287 | + * @param gridAllocation - optional [][] matrix to define allocated grid cells |
| 288 | + * @returns - Array of positions in px building the orthogonal path |
| 289 | + */ |
| 290 | +export function getOrthogonalPath( |
| 291 | + start: Point, |
| 292 | + end: Point, |
| 293 | + gridSize: number, |
| 294 | + gridAllocation?: (0 | 1)[][] |
| 295 | +): Point[] { |
| 296 | + if (start.x === end.x && start.y === end.y) return []; |
| 297 | + |
| 298 | + let trimmedStart = start; |
| 299 | + let trimmedEnd = end; |
| 300 | + |
| 301 | + if (!gridAllocation) { |
| 302 | + [trimmedStart, trimmedEnd] = trimStartEnd(start, end, gridSize); |
| 303 | + gridAllocation = emptyAllocation(trimmedStart, trimmedEnd, gridSize); |
| 304 | + } |
| 305 | + |
| 306 | + const graph: GraphNode[] = createGraph(gridAllocation!, gridSize); |
| 307 | + |
| 308 | + const shortesPath = getPath(graph, trimmedStart, trimmedEnd); |
| 309 | + |
| 310 | + return fullPath(shortesPath, start, end, trimmedStart, trimmedEnd); |
| 311 | +} |
0 commit comments