import { LinkRouter, RoutedLinks, Vertex } from '../customization/props'; import { DiagramModel } from './model'; import { Link as DiagramLink, Element as DiagramElement } from './elements'; import { Vector } from './geometry'; export class DefaultLinkRouter implements LinkRouter { constructor(private gap = 20) {} route(model: DiagramModel): RoutedLinks { const routings: RoutedLinks = new Map(); for (const link of model.links) { if (routings.has(link.id)) { continue; } // The cell is a link. Let's find its source and target models. const {sourceId, targetId} = link; if (!sourceId || !targetId) { continue; } else if (sourceId === targetId) { this.routeFeedbackSiblingLinks(model, sourceId, routings); } else { this.routeNormalSiblingLinks(model, sourceId, targetId, routings); } } return routings; } private routeFeedbackSiblingLinks(model: DiagramModel, elementId: string, routings: RoutedLinks) { const element = model.getElement(elementId); const {x, y} = element.position; const {width, height} = element.size; let index = 0; for (const sibling of element.links) { if (routings.has(sibling.id) || hasUserPlacedVertices(sibling)) { continue; } const {sourceId, targetId} = sibling; if (sourceId === targetId) { const offset = this.gap * (index + 1); const vertices: Vertex[] = [ {x: x - offset, y: y + height / 2}, {x: x - offset, y: y - offset}, {x: x + width / 2, y: y - offset}, ]; routings.set(sibling.id, {linkId: sibling.id, vertices}); index++; } } } private routeNormalSiblingLinks( model: DiagramModel, sourceId: string, targetId: string, routings: RoutedLinks, ) { const source = model.getElement(sourceId); const target = model.getElement(targetId); const sourceCenter = centerOfElement(source); const targetCenter = centerOfElement(target); const midPoint = { x: (sourceCenter.x + targetCenter.x) / 2, y: (sourceCenter.y + targetCenter.y) / 2, }; const direction = Vector.normalize({ x: targetCenter.x - sourceCenter.x, y: targetCenter.y - sourceCenter.y, }); const siblings = source.links.filter(link => (link.sourceId === targetId || link.targetId === targetId) && !routings.has(link.id) && !hasUserPlacedVertices(link) ); if (siblings.length <= 1) { return; } const indexModifier = siblings.length % 2 ? 0 : 1; siblings.forEach((sibling, siblingIndex) => { // For mor beautifull positioning const index = siblingIndex + indexModifier; // We want the offset values to be calculated as follows 0, 50, 50, 100, 100, 150, 150 .. const offset = this.gap * Math.ceil(index / 2) - (indexModifier ? this.gap / 2 : 0); // Now we need the vertices to be placed at points which are 'offset' pixels distant // from the first link and forms a perpendicular angle to it. And as index goes up // alternate left and right. // // ^ odd indexes // | // |----> index 0 line (straight line between a source center and a target center. // | // v even indexes const offsetDirection = index % 2 ? {x: -direction.y, y: direction.x} // rotate by 90 degrees counter-clockwise : {x: direction.y, y: -direction.x}; // rotate by 90 degrees clockwise // We found the vertex. const vertex = { x: midPoint.x + offsetDirection.x * offset, y: midPoint.y + offsetDirection.y * offset, }; routings.set(sibling.id, { linkId: sibling.id, vertices: [vertex], labelTextAnchor: this.getLabelAlignment(direction, siblingIndex, siblings.length), }); }); } private getLabelAlignment( connectionDirection: Vector, siblingIndex: number, siblingCount: number, ): 'start' | 'middle' | 'end' { // offset direction angle in [0; 2 Pi] interval const angle = Math.atan2(connectionDirection.y, connectionDirection.x); const absoluteAngle = Math.abs(angle); const isHorizontal = absoluteAngle < Math.PI * 1 / 8 || absoluteAngle > Math.PI * 7 / 8; const isTop = angle < 0; const isBottom = angle > 0; const firstOuter = siblingCount - 2; const secondOuter = siblingCount - 1; if (!isHorizontal) { if (isTop && siblingIndex === secondOuter || isBottom && siblingIndex === firstOuter) { return 'end'; } else if (isTop && siblingIndex === firstOuter || isBottom && siblingIndex === secondOuter) { return 'start'; } } return 'middle'; } } function hasUserPlacedVertices(link: DiagramLink) { const vertices = link.vertices; return vertices && vertices.length > 0; } function centerOfElement(element: DiagramElement): Vector { const {x, y} = element.position; const {width, height} = element.size; return {x: x + width / 2, y: y + height / 2}; }