UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

353 lines (305 loc) 9.92 kB
import { KeyValue } from '../../../types' import { FunctionExt } from '../../../util' import { Point, Rectangle } from '../../../geometry' import { EdgeView } from '../../../view' import { Router } from '../index' import { SortedSet } from './sorted-set' import { ObstacleMap } from './obstacle-map' import * as util from './util' import { resolveOptions, ResolvedOptions, ManhattanRouterOptions, } from './options' /** * Finds the route between two points (`from`, `to`). */ function findRoute( edgeView: EdgeView, from: Point | Rectangle, to: Point | Rectangle, map: ObstacleMap, options: ResolvedOptions, ) { const precision = options.precision let sourceEndpoint let targetEndpoint if (Rectangle.isRectangle(from)) { sourceEndpoint = util.round( util.getSourceEndpoint(edgeView, options).clone(), precision, ) } else { sourceEndpoint = util.round(from.clone(), precision) } if (Rectangle.isRectangle(to)) { targetEndpoint = util.round( util.getTargetEndpoint(edgeView, options).clone(), precision, ) } else { targetEndpoint = util.round(to.clone(), precision) } // Get grid for this route. const grid = util.getGrid(options.step, sourceEndpoint, targetEndpoint) // Get pathfinding points. // ----------------------- const startPoint = sourceEndpoint const endPoint = targetEndpoint let startPoints let endPoints if (Rectangle.isRectangle(from)) { startPoints = util.getRectPoints( startPoint, from, options.startDirections, grid, options, ) } else { startPoints = [startPoint] } if (Rectangle.isRectangle(to)) { endPoints = util.getRectPoints( targetEndpoint, to, options.endDirections, grid, options, ) } else { endPoints = [endPoint] } // take into account only accessible rect points (those not under obstacles) startPoints = startPoints.filter((p) => map.isAccessible(p)) endPoints = endPoints.filter((p) => map.isAccessible(p)) // There is an accessible route point on both sides. if (startPoints.length > 0 && endPoints.length > 0) { const openSet = new SortedSet() // Keeps the actual points for given nodes of the open set. const points: KeyValue<Point> = {} // Keeps the point that is immediate predecessor of given element. const parents: KeyValue<Point> = {} // Cost from start to a point along best known path. const costs: KeyValue<number> = {} for (let i = 0, n = startPoints.length; i < n; i += 1) { // startPoint is assumed to be aligned already const startPoint = startPoints[i] const key = util.getKey(startPoint) openSet.add(key, util.getCost(startPoint, endPoints)) points[key] = startPoint costs[key] = 0 } const previousRouteDirectionAngle = options.previousDirectionAngle // undefined for first route const isPathBeginning = previousRouteDirectionAngle === undefined // directions let direction let directionChange const directions = util.getGridOffsets(grid, options) const numDirections = directions.length const endPointsKeys = endPoints.reduce<string[]>((res, endPoint) => { const key = util.getKey(endPoint) res.push(key) return res }, []) // main route finding loop const sameStartEndPoints = Point.equalPoints(startPoints, endPoints) let loopsRemaining = options.maxLoopCount while (!openSet.isEmpty() && loopsRemaining > 0) { // Get the closest item and mark it CLOSED const currentKey = openSet.pop()! const currentPoint = points[currentKey] const currentParent = parents[currentKey] const currentCost = costs[currentKey] const isStartPoint = currentPoint.equals(startPoint) const isRouteBeginning = currentParent == null let previousDirectionAngle: number | null | undefined if (!isRouteBeginning) { previousDirectionAngle = util.getDirectionAngle( currentParent, currentPoint, numDirections, grid, options, ) } else if (!isPathBeginning) { // a vertex on the route previousDirectionAngle = previousRouteDirectionAngle } else if (!isStartPoint) { // beginning of route on the path previousDirectionAngle = util.getDirectionAngle( startPoint, currentPoint, numDirections, grid, options, ) } else { previousDirectionAngle = null } // Check if we reached any endpoint const skipEndCheck = isRouteBeginning && sameStartEndPoints if (!skipEndCheck && endPointsKeys.indexOf(currentKey) >= 0) { options.previousDirectionAngle = previousDirectionAngle return util.reconstructRoute( parents, points, currentPoint, startPoint, endPoint, ) } // Go over all possible directions and find neighbors for (let i = 0; i < numDirections; i += 1) { direction = directions[i] const directionAngle = direction.angle! directionChange = util.getDirectionChange( previousDirectionAngle!, directionAngle, ) // Don't use the point changed rapidly. if ( !(isPathBeginning && isStartPoint) && directionChange > options.maxDirectionChange ) { continue } const neighborPoint = util.align( currentPoint .clone() .translate(direction.gridOffsetX || 0, direction.gridOffsetY || 0), grid, precision, ) const neighborKey = util.getKey(neighborPoint) // Closed points were already evaluated. if (openSet.isClose(neighborKey) || !map.isAccessible(neighborPoint)) { continue } // Neighbor is an end point. if (endPointsKeys.indexOf(neighborKey) >= 0) { const isEndPoint = neighborPoint.equals(endPoint) if (!isEndPoint) { const endDirectionAngle = util.getDirectionAngle( neighborPoint, endPoint, numDirections, grid, options, ) const endDirectionChange = util.getDirectionChange( directionAngle, endDirectionAngle, ) if (endDirectionChange > options.maxDirectionChange) { continue } } } // The current direction is ok. // ---------------------------- const neighborCost = direction.cost const neighborPenalty = isStartPoint ? 0 : options.penalties[directionChange] const costFromStart = currentCost + neighborCost + neighborPenalty // Neighbor point has not been processed yet or the cost of // the path from start is lower than previously calculated. if ( !openSet.isOpen(neighborKey) || costFromStart < costs[neighborKey] ) { points[neighborKey] = neighborPoint parents[neighborKey] = currentPoint costs[neighborKey] = costFromStart openSet.add( neighborKey, costFromStart + util.getCost(neighborPoint, endPoints), ) } } loopsRemaining -= 1 } } if (options.fallbackRoute) { return FunctionExt.call( options.fallbackRoute, this, startPoint, endPoint, options, ) } return null } export const router: Router.Definition<ManhattanRouterOptions> = function ( vertices, optionsRaw, edgeView, ) { const options = resolveOptions(optionsRaw) const sourceBBox = util.getSourceBBox(edgeView, options) const targetBBox = util.getTargetBBox(edgeView, options) const sourceEndpoint = util.getSourceEndpoint(edgeView, options) // pathfinding const map = new ObstacleMap(options).build( edgeView.graph.model, edgeView.cell, ) const oldVertices = vertices.map((p) => Point.create(p)) const newVertices: Point[] = [] // The origin of first route's grid, does not need snapping let tailPoint = sourceEndpoint let from let to for (let i = 0, len = oldVertices.length; i <= len; i += 1) { let partialRoute: Point[] | null = null from = to || sourceBBox to = oldVertices[i] // This is the last iteration if (to == null) { to = targetBBox // If the target is a point, we should use dragging route // instead of main routing method if it has been provided. const edge = edgeView.cell const isEndingAtPoint = edge.getSourceCellId() == null || edge.getTargetCellId() == null if (isEndingAtPoint && typeof options.draggingRouter === 'function') { const dragFrom = from === sourceBBox ? sourceEndpoint : from const dragTo = to.getOrigin() partialRoute = FunctionExt.call( options.draggingRouter, edgeView, dragFrom, dragTo, options, ) } } // Find the partial route if (partialRoute == null) { partialRoute = findRoute(edgeView, from, to, map, options) } // Cannot found the partial route. if (partialRoute === null) { return FunctionExt.call( options.fallbackRouter, this, vertices, options, edgeView, ) } // Remove the first point if the previous partial route has // the same point as last. const leadPoint = partialRoute[0] if (leadPoint && leadPoint.equals(tailPoint)) { partialRoute.shift() } // Save tailPoint for next iteration tailPoint = partialRoute[partialRoute.length - 1] || tailPoint newVertices.push(...partialRoute) } return newVertices }