import { isDefined } from 'ts-is-present'; import { Link } from '../renderables/Link'; import { ShapeRenderer } from '../renderers/shapes/ShapeRenderer'; import { Edge } from '../types/Edge'; import { Node } from '../types/Node'; import { Point } from '../types/Point'; import { LinkSet } from './LinkSet'; abstract class Router { private _edgeLinkMap: Map; // The link that represents a given edge private _groupLinkMap: Map; // The set of links in a styling group private _linkEdgeMap: Map; // The set of edges that a given link represents private _nameNodeMap: Map; // The node with a given name private _nodeEdgeMap: Map; // The set of edges connected to a node private _ungroupedLinks: Link[]; // Links that aren't parent of a styling group protected _collapse: boolean; protected _bundleMap: LinkSet; protected _nodeShapeMap: Map; protected constructor () { this._bundleMap = new LinkSet(); this._edgeLinkMap = new Map(); this._groupLinkMap = new Map(); this._linkEdgeMap = new Map(); this._nameNodeMap = new Map(); this._nodeEdgeMap = new Map(); this._ungroupedLinks = []; this._collapse = true; this._nodeShapeMap = new Map(); } abstract calculateLink (link: Link, source: Node, target: Node, group: Link[]): void; /** * Get all links in the provided groups. If no groups are provided, all * links are returned. * @param groups An array of group names */ getLinks (groups?: string[]): Link[] { if (groups) { return groups .map(group => this._groupLinkMap.get(group)) .filter(isDefined) .reduce((acc, cur) => acc.concat(cur), []); } return Array.from(this._linkEdgeMap.keys()); } /** * Get a map of links to edge arrays. A single link can be used to represent * multiple edges, and this map can be used to determine which edges are * represented by any given link. */ getLinkMap (): Map { return this._linkEdgeMap; } /** * Get all links whose edges are not part of a group */ getUngroupedLinks (): Link[] { return this._ungroupedLinks; } /** * Set the nodes and edges and recalculate all links. * @param nodes An array of nodes * @param edges An array of edges * @param shapes The shape renderers used to render the nodes */ setData (nodes: Node[], edges: Edge[], shapes: ShapeRenderer[]): void { // Create a map of node names to nodes this._bundleMap = new LinkSet(); this._edgeLinkMap = new Map(); this._groupLinkMap = new Map(); this._linkEdgeMap = new Map(); this._nameNodeMap = new Map(nodes.map(node => [node.id, node])); this._nodeEdgeMap = buildNodeEdgeMap(this._nameNodeMap, edges); this._ungroupedLinks = []; // Create the map of nodes to shapes, which subclasses will // likely need when calculating routes this._nodeShapeMap = new Map(); shapes.forEach(shape => shape.nodes().forEach(node => this._nodeShapeMap.set(node, shape))); // Place edges into groups that represent collapsed edges const collapsed = this._collapse ? collapseEdges(edges) : edges.map(edge => [edge]); // Create a single link for each set of edges collapsed.forEach((edgeset: Edge[]) => { if (edgeset.length) { const groupname = edgeset[0].group; const sourceName = edgeset[0].source; const targetName = edgeset[0].target; const source = this._nameNodeMap.get(sourceName); const target = this._nameNodeMap.get(targetName); if (source && target) { // Create the link (initially empty) const link: Link = { path: undefined, labels: [] }; // Save the link -> edgeset this._linkEdgeMap.set(link, edgeset); // Save the edge -> link edgeset.forEach(edge => { this._edgeLinkMap.set(edge, link); }); // Save to bundle map this._bundleMap.add(source, target, link); // Save to group if (groupname) { if (!this._groupLinkMap.has(groupname)) this._groupLinkMap.set(groupname, []); this._groupLinkMap.get(groupname)!.push(link); } else { this._ungroupedLinks.push(link); } } } }); this.updateLinks(); } /** * Calculate all links */ updateLinks (): void { this._bundleMap.forEach(this.calculateLink.bind(this)); } /** * Calculate links connected to the given node. Returns the array of links * that were recalculated. * @param node A node */ updateLinksConnectedTo (node: Node): Link[] { // Get the set of edges that connect to the node const edges = this._nodeEdgeMap.get(node); if (edges) { const recalculated: Set = new Set(); edges.forEach(edge => { const link = this._edgeLinkMap.get(edge); if (link && !recalculated.has(link)) { const source = this._nameNodeMap.get(edge.source); const target = this._nameNodeMap.get(edge.target); if (source && target) { const group = this._bundleMap.allBetween(source, target); this.calculateLink(link, source, target, group); } recalculated.add(link); } }); return Array.from(recalculated); } return []; } /** * Calculate the pixel offsets for the first and last points of a link. * These offsets place links (the origins and arrowheads) at the edges * of the shapes rather than the centers. * @param link The link to process * @param source The source point shape renderer * @param target The target point shape renderer * @private */ protected _calcalateShapeOffsets (link: Link, source?: ShapeRenderer, target?: ShapeRenderer): void { const points = link.path ? link.path.points() : []; if (points.length < 2) return; if (source) { const src = points[0]; const nxt = points[1]; calculateShapeOffset(src, nxt, source); } if (target) { const trg = points[points.length - 1]; const prv = points[points.length - 2]; calculateShapeOffset(trg, prv, target); } } } function buildNodeEdgeMap (nodes: Map, edges: Edge[]): Map { const map = new Map(); edges.forEach(edge => { const source = nodes.get(edge.source); const target = nodes.get(edge.target); if (source && target) { if (!map.has(source)) map.set(source, []); if (!map.has(target)) map.set(target, []); map.get(source)!.push(edge); map.get(target)!.push(edge); } }); return map; } function calculateShapeOffset (center: Point, other: Point, shape: ShapeRenderer): void { const ng = Math.atan2(center.y - other.y, other.x - center.x); const offset = shape.intersect(ng); center.dx = offset.x; center.dy = offset.y; } function collapseEdges (edges: Edge[]): Edge[][] { const groups = new Map(); const ungrouped: Edge[] = []; // Place edges into groups edges.forEach(edge => { if (edge.group) { if (!groups.has(edge.group)) groups.set(edge.group, []); groups.get(edge.group)!.push(edge); } else { ungrouped.push(edge); } }); // For each group, collapse edges that have the same source and target const allgroups: Edge[][] = [ungrouped, ...Array.from(groups.values())]; const collapsed: Edge[][] = []; allgroups.forEach(edgegroup => { const edgemap = new Map(); edgegroup.forEach(edge => { const id = edge.source + edge.target; if (!edgemap.has(id)) edgemap.set(id, []); edgemap.get(id)!.push(edge); }); collapsed.push(...Array.from(edgemap.values())); }); return collapsed; } export { Router }