@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering.
1,315 lines • 77.6 kB
JavaScript
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import { Rectangle, Polyline, Point, Angle, Path, Line } from '../geometry';
import { StringExt, ObjectExt, NumberExt, FunctionExt, Dom, Vector, } from '../util';
import { Router, Connector, NodeAnchor, EdgeAnchor, ConnectionPoint, } from '../registry';
import { Edge } from '../model/edge';
import { Markup } from './markup';
import { CellView } from './cell';
export class EdgeView extends CellView {
constructor() {
super(...arguments);
this.POINT_ROUNDING = 2;
this.markerCache = {};
// #endregion
// #endregion
}
get [Symbol.toStringTag]() {
return EdgeView.toStringTag;
}
getContainerClassName() {
return [super.getContainerClassName(), this.prefixClassName('edge')].join(' ');
}
get sourceBBox() {
const sourceView = this.sourceView;
if (!sourceView) {
const sourceDef = this.cell.getSource();
return new Rectangle(sourceDef.x, sourceDef.y);
}
const sourceMagnet = this.sourceMagnet;
if (sourceView.isEdgeElement(sourceMagnet)) {
return new Rectangle(this.sourceAnchor.x, this.sourceAnchor.y);
}
return sourceView.getBBoxOfElement(sourceMagnet || sourceView.container);
}
get targetBBox() {
const targetView = this.targetView;
if (!targetView) {
const targetDef = this.cell.getTarget();
return new Rectangle(targetDef.x, targetDef.y);
}
const targetMagnet = this.targetMagnet;
if (targetView.isEdgeElement(targetMagnet)) {
return new Rectangle(this.targetAnchor.x, this.targetAnchor.y);
}
return targetView.getBBoxOfElement(targetMagnet || targetView.container);
}
isEdgeView() {
return true;
}
confirmUpdate(flag, options = {}) {
let ref = flag;
if (this.hasAction(ref, 'source')) {
if (!this.updateTerminalProperties('source')) {
return ref;
}
ref = this.removeAction(ref, 'source');
}
if (this.hasAction(ref, 'target')) {
if (!this.updateTerminalProperties('target')) {
return ref;
}
ref = this.removeAction(ref, 'target');
}
const graph = this.graph;
const sourceView = this.sourceView;
const targetView = this.targetView;
if (graph &&
((sourceView && !graph.renderer.isViewMounted(sourceView)) ||
(targetView && !graph.renderer.isViewMounted(targetView)))) {
// Wait for the sourceView and targetView to be rendered.
return ref;
}
if (this.hasAction(ref, 'render')) {
this.render();
ref = this.removeAction(ref, [
'render',
'update',
'vertices',
'labels',
'tools',
'widget',
]);
return ref;
}
ref = this.handleAction(ref, 'vertices', () => this.renderVertexMarkers());
ref = this.handleAction(ref, 'update', () => this.update(null, options));
ref = this.handleAction(ref, 'labels', () => this.onLabelsChange(options));
ref = this.handleAction(ref, 'tools', () => {
this.renderTools();
this.updateToolsPosition();
});
ref = this.handleAction(ref, 'widget', () => this.renderExternalTools());
return ref;
}
onLabelsChange(options = {}) {
// Note: this optimization works in async=false mode only
if (this.shouldRerenderLabels(options)) {
this.renderLabels();
}
else {
this.updateLabels();
}
this.updateLabelPositions();
}
shouldRerenderLabels(options = {}) {
const previousLabels = this.cell.previous('labels');
if (previousLabels == null) {
return true;
}
// Here is an optimization for cases when we know, that change does
// not require re-rendering of all labels.
if ('propertyPathArray' in options && 'propertyValue' in options) {
// The label is setting by `prop()` method
const pathArray = options.propertyPathArray || [];
const pathLength = pathArray.length;
if (pathLength > 1) {
// We are changing a single label here e.g. 'labels/0/position'
const index = pathArray[1];
if (previousLabels[index]) {
if (pathLength === 2) {
// We are changing the entire label. Need to check if the
// markup is also being changed.
return (typeof options.propertyValue === 'object' &&
ObjectExt.has(options.propertyValue, 'markup'));
}
// We are changing a label property but not the markup
if (pathArray[2] !== 'markup') {
return false;
}
}
}
}
return true;
}
render() {
this.empty();
this.containers = {};
this.renderMarkup();
this.renderLabels();
this.update();
return this;
}
renderMarkup() {
const markup = this.cell.markup;
if (markup) {
if (typeof markup === 'string') {
return this.renderStringMarkup(markup);
}
return this.renderJSONMarkup(markup);
}
throw new TypeError('Invalid edge markup.');
}
renderJSONMarkup(markup) {
const ret = this.parseJSONMarkup(markup, this.container);
this.selectors = ret.selectors;
this.container.append(ret.fragment);
}
renderStringMarkup(markup) {
const cache = this.containers;
const children = Vector.createVectors(markup);
// Cache children elements for quicker access.
children.forEach((child) => {
const className = child.attr('class');
if (className) {
cache[StringExt.camelCase(className)] =
child.node;
}
});
this.renderTools();
this.renderVertexMarkers();
this.renderArrowheadMarkers();
Dom.append(this.container, children.map((child) => child.node));
}
renderLabels() {
const edge = this.cell;
const labels = edge.getLabels();
const count = labels.length;
let container = this.containers.labels;
this.labelCache = {};
this.labelSelectors = {};
if (count <= 0) {
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
return this;
}
if (container) {
this.empty(container);
}
else {
container = Dom.createSvgElement('g');
this.addClass(this.prefixClassName('edge-labels'), container);
this.containers.labels = container;
}
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i];
const normalized = this.normalizeLabelMarkup(this.parseLabelMarkup(label.markup));
let labelNode;
let selectors;
if (normalized) {
labelNode = normalized.node;
selectors = normalized.selectors;
}
else {
const defaultLabel = edge.getDefaultLabel();
const normalized = this.normalizeLabelMarkup(this.parseLabelMarkup(defaultLabel.markup));
labelNode = normalized.node;
selectors = normalized.selectors;
}
labelNode.setAttribute('data-index', `${i}`);
container.appendChild(labelNode);
const rootSelector = this.rootSelector;
if (selectors[rootSelector]) {
throw new Error('Ambiguous label root selector.');
}
selectors[rootSelector] = labelNode;
this.labelCache[i] = labelNode;
this.labelSelectors[i] = selectors;
}
if (container.parentNode == null) {
this.container.appendChild(container);
}
this.updateLabels();
this.customizeLabels();
return this;
}
parseLabelMarkup(markup) {
if (markup) {
if (typeof markup === 'string') {
return this.parseLabelStringMarkup(markup);
}
return this.parseJSONMarkup(markup);
}
return null;
}
parseLabelStringMarkup(labelMarkup) {
const children = Vector.createVectors(labelMarkup);
const fragment = document.createDocumentFragment();
for (let i = 0, n = children.length; i < n; i += 1) {
const currentChild = children[i].node;
fragment.appendChild(currentChild);
}
return { fragment, selectors: {} };
}
normalizeLabelMarkup(markup) {
if (markup == null) {
return;
}
const fragment = markup.fragment;
if (!(fragment instanceof DocumentFragment) || !fragment.hasChildNodes()) {
throw new Error('Invalid label markup.');
}
let vel;
const childNodes = fragment.childNodes;
if (childNodes.length > 1 || childNodes[0].nodeName.toUpperCase() !== 'G') {
// default markup fragment is not wrapped in `<g/>`
// add a `<g/>` container
vel = Vector.create('g').append(fragment);
}
else {
vel = Vector.create(childNodes[0]);
}
vel.addClass(this.prefixClassName('edge-label'));
return {
node: vel.node,
selectors: markup.selectors,
};
}
updateLabels() {
if (this.containers.labels) {
const edge = this.cell;
const labels = edge.labels;
const canLabelMove = this.can('edgeLabelMovable');
const defaultLabel = edge.getDefaultLabel();
for (let i = 0, n = labels.length; i < n; i += 1) {
const elem = this.labelCache[i];
const selectors = this.labelSelectors[i];
elem.setAttribute('cursor', canLabelMove ? 'move' : 'default');
const label = labels[i];
const attrs = ObjectExt.merge({}, defaultLabel.attrs, label.attrs);
this.updateAttrs(elem, attrs, {
selectors,
rootBBox: label.size ? Rectangle.fromSize(label.size) : undefined,
});
}
}
}
mergeLabelAttrs(hasCustomMarkup, labelAttrs, defaultLabelAttrs) {
if (labelAttrs === null) {
return null;
}
if (labelAttrs === undefined) {
if (defaultLabelAttrs === null) {
return null;
}
if (defaultLabelAttrs === undefined) {
return undefined;
}
if (hasCustomMarkup) {
return defaultLabelAttrs;
}
return ObjectExt.merge({}, defaultLabelAttrs);
}
if (hasCustomMarkup) {
return ObjectExt.merge({}, defaultLabelAttrs, labelAttrs);
}
}
customizeLabels() {
if (this.containers.labels) {
const edge = this.cell;
const labels = edge.labels;
for (let i = 0, n = labels.length; i < n; i += 1) {
const label = labels[i];
const container = this.labelCache[i];
const selectors = this.labelSelectors[i];
this.graph.hook.onEdgeLabelRendered({
edge,
label,
container,
selectors,
});
}
}
}
renderTools() {
const container = this.containers.tools;
if (container == null) {
return this;
}
const markup = this.cell.toolMarkup;
const $container = this.$(container).empty();
if (Markup.isStringMarkup(markup)) {
let template = StringExt.template(markup);
const tool = Vector.create(template());
$container.append(tool.node);
this.toolCache = tool.node;
// If `doubleTools` is enabled, we render copy of the tools on the
// other side of the edge as well but only if the edge is longer
// than `longLength`.
if (this.options.doubleTools) {
let tool2;
const doubleToolMarkup = this.cell.doubleToolMarkup;
if (Markup.isStringMarkup(doubleToolMarkup)) {
template = StringExt.template(doubleToolMarkup);
tool2 = Vector.create(template());
}
else {
tool2 = tool.clone();
}
$container.append(tool2.node);
this.tool2Cache = tool2.node;
}
}
return this;
}
renderExternalTools() {
const tools = this.cell.getTools();
this.addTools(tools);
return this;
}
renderVertexMarkers() {
const container = this.containers.vertices;
if (container == null) {
return this;
}
const markup = this.cell.vertexMarkup;
const $container = this.$(container).empty();
if (Markup.isStringMarkup(markup)) {
const template = StringExt.template(markup);
this.cell.getVertices().forEach((vertex, index) => {
$container.append(Vector.create(template(Object.assign({ index }, vertex))).node);
});
}
return this;
}
renderArrowheadMarkers() {
const container = this.containers.arrowheads;
if (container == null) {
return this;
}
const markup = this.cell.arrowheadMarkup;
const $container = this.$(container).empty();
if (Markup.isStringMarkup(markup)) {
const template = StringExt.template(markup);
const sourceArrowhead = Vector.create(template({ end: 'source' })).node;
const targetArrowhead = Vector.create(template({ end: 'target' })).node;
this.containers.sourceArrowhead = sourceArrowhead;
this.containers.targetArrowhead = targetArrowhead;
$container.append(sourceArrowhead, targetArrowhead);
}
return this;
}
// #endregion
// #region updating
update(partialAttrs, options = {}) {
this.cleanCache();
this.updateConnection(options);
const attrs = this.cell.getAttrs();
if (attrs != null) {
this.updateAttrs(this.container, attrs, {
attrs: partialAttrs === attrs ? null : partialAttrs,
selectors: this.selectors,
});
}
this.updateConnectionPath();
this.updateLabelPositions();
this.updateToolsPosition();
this.updateArrowheadMarkers();
if (options.toolId == null) {
this.renderExternalTools();
}
else {
this.updateTools(options);
}
return this;
}
removeRedundantLinearVertices(options = {}) {
const edge = this.cell;
const vertices = edge.getVertices();
const routePoints = [this.sourceAnchor, ...vertices, this.targetAnchor];
const rawCount = routePoints.length;
// Puts the route points into a polyline and try to simplify.
const polyline = new Polyline(routePoints);
polyline.simplify({ threshold: 0.01 });
const simplifiedPoints = polyline.points.map((point) => point.toJSON());
const simplifiedCount = simplifiedPoints.length;
// If simplification did not remove any redundant vertices.
if (rawCount === simplifiedCount) {
return 0;
}
// Sets simplified polyline points as edge vertices.
// Removes first and last polyline points again (source/target anchors).
edge.setVertices(simplifiedPoints.slice(1, simplifiedCount - 1), options);
return rawCount - simplifiedCount;
}
updateConnectionPath() {
const containers = this.containers;
if (containers.connection) {
const pathData = this.getConnectionPathData();
containers.connection.setAttribute('d', pathData);
}
if (containers.connectionWrap) {
const pathData = this.getConnectionPathData();
containers.connectionWrap.setAttribute('d', pathData);
}
if (containers.sourceMarker && containers.targetMarker) {
this.translateAndAutoOrientArrows(containers.sourceMarker, containers.targetMarker);
}
}
getTerminalView(type) {
switch (type) {
case 'source':
return this.sourceView || null;
case 'target':
return this.targetView || null;
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalAnchor(type) {
switch (type) {
case 'source':
return Point.create(this.sourceAnchor);
case 'target':
return Point.create(this.targetAnchor);
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalConnectionPoint(type) {
switch (type) {
case 'source':
return Point.create(this.sourcePoint);
case 'target':
return Point.create(this.targetPoint);
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalMagnet(type, options = {}) {
switch (type) {
case 'source': {
if (options.raw) {
return this.sourceMagnet;
}
const sourceView = this.sourceView;
if (!sourceView) {
return null;
}
return this.sourceMagnet || sourceView.container;
}
case 'target': {
if (options.raw) {
return this.targetMagnet;
}
const targetView = this.targetView;
if (!targetView) {
return null;
}
return this.targetMagnet || targetView.container;
}
default: {
throw new Error(`Unknown terminal type '${type}'`);
}
}
}
updateConnection(options = {}) {
const edge = this.cell;
// The edge is being translated by an ancestor that will shift
// source, target and vertices by an equal distance.
if (options.translateBy &&
edge.isFragmentDescendantOf(options.translateBy)) {
const tx = options.tx || 0;
const ty = options.ty || 0;
this.routePoints = new Polyline(this.routePoints).translate(tx, ty).points;
this.translateConnectionPoints(tx, ty);
this.path.translate(tx, ty);
}
else {
const vertices = edge.getVertices();
// 1. Find anchor points
const anchors = this.findAnchors(vertices);
this.sourceAnchor = anchors.source;
this.targetAnchor = anchors.target;
// 2. Find route points
this.routePoints = this.findRoutePoints(vertices);
// 3. Find connection points
const connectionPoints = this.findConnectionPoints(this.routePoints, this.sourceAnchor, this.targetAnchor);
this.sourcePoint = connectionPoints.source;
this.targetPoint = connectionPoints.target;
// 4. Find Marker Connection Point
const markerPoints = this.findMarkerPoints(this.routePoints, this.sourcePoint, this.targetPoint);
// 5. Make path
this.path = this.findPath(this.routePoints, markerPoints.source || this.sourcePoint, markerPoints.target || this.targetPoint);
}
this.cleanCache();
}
findAnchors(vertices) {
const edge = this.cell;
const source = edge.source;
const target = edge.target;
const firstVertex = vertices[0];
const lastVertex = vertices[vertices.length - 1];
if (target.priority && !source.priority) {
// Reversed order
return this.findAnchorsOrdered('target', lastVertex, 'source', firstVertex);
}
// Usual order
return this.findAnchorsOrdered('source', firstVertex, 'target', lastVertex);
}
findAnchorsOrdered(firstType, firstPoint, secondType, secondPoint) {
let firstAnchor;
let secondAnchor;
const edge = this.cell;
const firstTerminal = edge[firstType];
const secondTerminal = edge[secondType];
const firstView = this.getTerminalView(firstType);
const secondView = this.getTerminalView(secondType);
const firstMagnet = this.getTerminalMagnet(firstType);
const secondMagnet = this.getTerminalMagnet(secondType);
if (firstView) {
let firstRef;
if (firstPoint) {
firstRef = Point.create(firstPoint);
}
else if (secondView) {
firstRef = secondMagnet;
}
else {
firstRef = Point.create(secondTerminal);
}
firstAnchor = this.getAnchor(firstTerminal.anchor, firstView, firstMagnet, firstRef, firstType);
}
else {
firstAnchor = Point.create(firstTerminal);
}
if (secondView) {
const secondRef = Point.create(secondPoint || firstAnchor);
secondAnchor = this.getAnchor(secondTerminal.anchor, secondView, secondMagnet, secondRef, secondType);
}
else {
secondAnchor = Point.isPointLike(secondTerminal)
? Point.create(secondTerminal)
: new Point();
}
return {
[firstType]: firstAnchor,
[secondType]: secondAnchor,
};
}
getAnchor(def, cellView, magnet, ref, terminalType) {
const isEdge = cellView.isEdgeElement(magnet);
const connecting = this.graph.options.connecting;
let config = typeof def === 'string' ? { name: def } : def;
if (!config) {
const defaults = isEdge
? (terminalType === 'source'
? connecting.sourceEdgeAnchor
: connecting.targetEdgeAnchor) || connecting.edgeAnchor
: (terminalType === 'source'
? connecting.sourceAnchor
: connecting.targetAnchor) || connecting.anchor;
config = typeof defaults === 'string' ? { name: defaults } : defaults;
}
if (!config) {
throw new Error(`Anchor should be specified.`);
}
let anchor;
const name = config.name;
if (isEdge) {
const fn = EdgeAnchor.registry.get(name);
if (typeof fn !== 'function') {
return EdgeAnchor.registry.onNotFound(name);
}
anchor = FunctionExt.call(fn, this, cellView, magnet, ref, config.args || {}, terminalType);
}
else {
const fn = NodeAnchor.registry.get(name);
if (typeof fn !== 'function') {
return NodeAnchor.registry.onNotFound(name);
}
anchor = FunctionExt.call(fn, this, cellView, magnet, ref, config.args || {}, terminalType);
}
return anchor ? anchor.round(this.POINT_ROUNDING) : new Point();
}
findRoutePoints(vertices = []) {
const defaultRouter = this.graph.options.connecting.router || Router.presets.normal;
const router = this.cell.getRouter() || defaultRouter;
let routePoints;
if (typeof router === 'function') {
routePoints = FunctionExt.call(router, this, vertices, {}, this);
}
else {
const name = typeof router === 'string' ? router : router.name;
const args = typeof router === 'string' ? {} : router.args || {};
const fn = name ? Router.registry.get(name) : Router.presets.normal;
if (typeof fn !== 'function') {
return Router.registry.onNotFound(name);
}
routePoints = FunctionExt.call(fn, this, vertices, args, this);
}
return routePoints == null
? vertices.map((p) => Point.create(p))
: routePoints.map((p) => Point.create(p));
}
findConnectionPoints(routePoints, sourceAnchor, targetAnchor) {
const edge = this.cell;
const connecting = this.graph.options.connecting;
const sourceTerminal = edge.getSource();
const targetTerminal = edge.getTarget();
const sourceView = this.sourceView;
const targetView = this.targetView;
const firstRoutePoint = routePoints[0];
const lastRoutePoint = routePoints[routePoints.length - 1];
// source
let sourcePoint;
if (sourceView && !sourceView.isEdgeElement(this.sourceMagnet)) {
const sourceMagnet = this.sourceMagnet || sourceView.container;
const sourcePointRef = firstRoutePoint || targetAnchor;
const sourceLine = new Line(sourcePointRef, sourceAnchor);
const connectionPointDef = sourceTerminal.connectionPoint ||
connecting.sourceConnectionPoint ||
connecting.connectionPoint;
sourcePoint = this.getConnectionPoint(connectionPointDef, sourceView, sourceMagnet, sourceLine, 'source');
}
else {
sourcePoint = sourceAnchor;
}
// target
let targetPoint;
if (targetView && !targetView.isEdgeElement(this.targetMagnet)) {
const targetMagnet = this.targetMagnet || targetView.container;
const targetConnectionPointDef = targetTerminal.connectionPoint ||
connecting.targetConnectionPoint ||
connecting.connectionPoint;
const targetPointRef = lastRoutePoint || sourceAnchor;
const targetLine = new Line(targetPointRef, targetAnchor);
targetPoint = this.getConnectionPoint(targetConnectionPointDef, targetView, targetMagnet, targetLine, 'target');
}
else {
targetPoint = targetAnchor;
}
return {
source: sourcePoint,
target: targetPoint,
};
}
getConnectionPoint(def, view, magnet, line, endType) {
const anchor = line.end;
if (def == null) {
return anchor;
}
const name = typeof def === 'string' ? def : def.name;
const args = typeof def === 'string' ? {} : def.args;
const fn = ConnectionPoint.registry.get(name);
if (typeof fn !== 'function') {
return ConnectionPoint.registry.onNotFound(name);
}
const connectionPoint = FunctionExt.call(fn, this, line, view, magnet, args || {}, endType);
return connectionPoint ? connectionPoint.round(this.POINT_ROUNDING) : anchor;
}
updateMarkerAttr(type) {
const attrs = this.cell.getAttrs();
const key = `.${type}-marker`;
const partial = attrs && attrs[key];
if (partial) {
this.updateAttrs(this.container, {}, {
attrs: { [key]: partial },
selectors: this.selectors,
});
}
}
findMarkerPoints(routePoints, sourcePoint, targetPoint) {
const getLineWidth = (type) => {
const attrs = this.cell.getAttrs();
const keys = Object.keys(attrs);
for (let i = 0, l = keys.length; i < l; i += 1) {
const attr = attrs[keys[i]];
if (attr[`${type}Marker`] || attr[`${type}-marker`]) {
const strokeWidth = attr.strokeWidth || attr['stroke-width'];
if (strokeWidth) {
return parseFloat(strokeWidth);
}
break;
}
}
return null;
};
const firstRoutePoint = routePoints[0];
const lastRoutePoint = routePoints[routePoints.length - 1];
const sourceMarkerElem = this.containers.sourceMarker;
const targetMarkerElem = this.containers.targetMarker;
const cache = this.markerCache;
let sourceMarkerPoint;
let targetMarkerPoint;
// Move the source point by the width of the marker taking into
// account its scale around x-axis. Note that scale is the only
// transform that makes sense to be set in `.marker-source`
// attributes object as all other transforms (translate/rotate)
// will be replaced by the `translateAndAutoOrient()` function.
if (sourceMarkerElem) {
this.updateMarkerAttr('source');
// support marker connection point registry???
cache.sourceBBox = cache.sourceBBox || Dom.getBBox(sourceMarkerElem);
if (cache.sourceBBox.width > 0) {
const scale = Dom.scale(sourceMarkerElem);
sourceMarkerPoint = sourcePoint
.clone()
.move(firstRoutePoint || targetPoint, cache.sourceBBox.width * scale.sx * -1);
}
}
else {
const strokeWidth = getLineWidth('source');
if (strokeWidth) {
sourceMarkerPoint = sourcePoint
.clone()
.move(firstRoutePoint || targetPoint, -strokeWidth);
}
}
if (targetMarkerElem) {
this.updateMarkerAttr('target');
cache.targetBBox = cache.targetBBox || Dom.getBBox(targetMarkerElem);
if (cache.targetBBox.width > 0) {
const scale = Dom.scale(targetMarkerElem);
targetMarkerPoint = targetPoint
.clone()
.move(lastRoutePoint || sourcePoint, cache.targetBBox.width * scale.sx * -1);
}
}
else {
const strokeWidth = getLineWidth('target');
if (strokeWidth) {
targetMarkerPoint = targetPoint
.clone()
.move(lastRoutePoint || sourcePoint, -strokeWidth);
}
}
// If there was no markup for the marker, use the connection point.
cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone();
cache.targetPoint = targetMarkerPoint || targetPoint.clone();
return {
source: sourceMarkerPoint,
target: targetMarkerPoint,
};
}
findPath(routePoints, sourcePoint, targetPoint) {
const def = this.cell.getConnector() || this.graph.options.connecting.connector;
let name;
let args;
let fn;
if (typeof def === 'string') {
name = def;
}
else {
name = def.name;
args = def.args;
}
if (name) {
const method = Connector.registry.get(name);
if (typeof method !== 'function') {
return Connector.registry.onNotFound(name);
}
fn = method;
}
else {
fn = Connector.presets.normal;
}
const path = FunctionExt.call(fn, this, sourcePoint, targetPoint, routePoints, Object.assign(Object.assign({}, args), { raw: true }), this);
return typeof path === 'string' ? Path.parse(path) : path;
}
translateConnectionPoints(tx, ty) {
const cache = this.markerCache;
if (cache.sourcePoint) {
cache.sourcePoint.translate(tx, ty);
}
if (cache.targetPoint) {
cache.targetPoint.translate(tx, ty);
}
this.sourcePoint.translate(tx, ty);
this.targetPoint.translate(tx, ty);
this.sourceAnchor.translate(tx, ty);
this.targetAnchor.translate(tx, ty);
}
updateLabelPositions() {
if (this.containers.labels == null) {
return this;
}
const path = this.path;
if (!path) {
return this;
}
const edge = this.cell;
const labels = edge.getLabels();
if (labels.length === 0) {
return this;
}
const defaultLabel = edge.getDefaultLabel();
const defaultPosition = this.normalizeLabelPosition(defaultLabel.position);
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i];
const labelPosition = this.normalizeLabelPosition(label.position);
const pos = ObjectExt.merge({}, defaultPosition, labelPosition);
const matrix = this.getLabelTransformationMatrix(pos);
this.labelCache[i].setAttribute('transform', Dom.matrixToTransformString(matrix));
}
return this;
}
updateToolsPosition() {
if (this.containers.tools == null) {
return this;
}
// Move the tools a bit to the target position but don't cover the
// `sourceArrowhead` marker. Note that the offset is hardcoded here.
// The offset should be always more than the
// `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking
// this up all the time would be slow.
let scale = '';
let offset = this.options.toolsOffset;
const connectionLength = this.getConnectionLength();
// Firefox returns `connectionLength=NaN` in odd cases (for bezier curves).
// In that case we won't update tools position at all.
if (connectionLength != null) {
// If the edge is too short, make the tools half the
// size and the offset twice as low.
if (connectionLength < this.options.shortLength) {
scale = 'scale(.5)';
offset /= 2;
}
let pos = this.getPointAtLength(offset);
if (pos != null) {
Dom.attr(this.toolCache, 'transform', `translate(${pos.x},${pos.y}) ${scale}`);
}
if (this.options.doubleTools &&
connectionLength >= this.options.longLength) {
const doubleToolsOffset = this.options.doubleToolsOffset || offset;
pos = this.getPointAtLength(connectionLength - doubleToolsOffset);
if (pos != null) {
Dom.attr(this.tool2Cache, 'transform', `translate(${pos.x},${pos.y}) ${scale}`);
}
Dom.attr(this.tool2Cache, 'visibility', 'visible');
}
else if (this.options.doubleTools) {
Dom.attr(this.tool2Cache, 'visibility', 'hidden');
}
}
return this;
}
updateArrowheadMarkers() {
const container = this.containers.arrowheads;
if (container == null) {
return this;
}
if (container.style.display === 'none') {
return this;
}
const sourceArrowhead = this.containers.sourceArrowhead;
const targetArrowhead = this.containers.targetArrowhead;
if (sourceArrowhead && targetArrowhead) {
const len = this.getConnectionLength() || 0;
const sx = len < this.options.shortLength ? 0.5 : 1;
Dom.scale(sourceArrowhead, sx);
Dom.scale(targetArrowhead, sx);
this.translateAndAutoOrientArrows(sourceArrowhead, targetArrowhead);
}
return this;
}
updateTerminalProperties(type) {
const edge = this.cell;
const graph = this.graph;
const terminal = edge[type];
const nodeId = terminal && terminal.cell;
const viewKey = `${type}View`;
// terminal is a point
if (!nodeId) {
this[viewKey] = null;
this.updateTerminalMagnet(type);
return true;
}
const terminalCell = graph.getCellById(nodeId);
if (!terminalCell) {
throw new Error(`Edge's ${type} node with id "${nodeId}" not exists`);
}
const endView = terminalCell.findView(graph);
if (!endView) {
return false;
}
this[viewKey] = endView;
this.updateTerminalMagnet(type);
return true;
}
updateTerminalMagnet(type) {
const propName = `${type}Magnet`;
const terminalView = this.getTerminalView(type);
if (terminalView) {
let magnet = terminalView.getMagnetFromEdgeTerminal(this.cell[type]);
if (magnet === terminalView.container) {
magnet = null;
}
this[propName] = magnet;
}
else {
this[propName] = null;
}
}
translateAndAutoOrientArrows(sourceArrow, targetArrow) {
const route = this.routePoints;
if (sourceArrow) {
Dom.translateAndAutoOrient(sourceArrow, this.sourcePoint, route[0] || this.targetPoint, this.graph.view.stage);
}
if (targetArrow) {
Dom.translateAndAutoOrient(targetArrow, this.targetPoint, route[route.length - 1] || this.sourcePoint, this.graph.view.stage);
}
}
getLabelPositionAngle(idx) {
const label = this.cell.getLabelAt(idx);
if (label && label.position && typeof label.position === 'object') {
return label.position.angle || 0;
}
return 0;
}
getLabelPositionArgs(idx) {
const label = this.cell.getLabelAt(idx);
if (label && label.position && typeof label.position === 'object') {
return label.position.options;
}
}
getDefaultLabelPositionArgs() {
const defaultLabel = this.cell.getDefaultLabel();
if (defaultLabel &&
defaultLabel.position &&
typeof defaultLabel.position === 'object') {
return defaultLabel.position.options;
}
}
// merge default label position args into label position args
// keep `undefined` or `null` because `{}` means something else
mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs) {
if (labelPositionArgs === null) {
return null;
}
if (labelPositionArgs === undefined) {
if (defaultLabelPositionArgs === null) {
return null;
}
return defaultLabelPositionArgs;
}
return ObjectExt.merge({}, defaultLabelPositionArgs, labelPositionArgs);
}
addLabel(p1, p2, p3, options) {
let localX;
let localY;
let localAngle = 0;
let localOptions;
if (typeof p1 !== 'number') {
localX = p1.x;
localY = p1.y;
if (typeof p2 === 'number') {
localAngle = p2;
localOptions = p3;
}
else {
localOptions = p2;
}
}
else {
localX = p1;
localY = p2;
if (typeof p3 === 'number') {
localAngle = p3;
localOptions = options;
}
else {
localOptions = p3;
}
}
// merge label position arguments
const defaultLabelPositionArgs = this.getDefaultLabelPositionArgs();
const labelPositionArgs = localOptions;
const positionArgs = this.mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs);
// append label to labels array
const label = {
position: this.getLabelPosition(localX, localY, localAngle, positionArgs),
};
const index = -1;
this.cell.insertLabel(label, index, localOptions);
return index;
}
addVertex(x, y, options) {
const isPoint = typeof x !== 'number';
const localX = isPoint ? x.x : x;
const localY = isPoint ? x.y : y;
const localOptions = isPoint ? y : options;
const vertex = { x: localX, y: localY };
const index = this.getVertexIndex(localX, localY);
this.cell.insertVertex(vertex, index, localOptions);
return index;
}
sendToken(token, options, callback) {
let duration;
let reversed;
let selector;
let rorate;
let timing = 'linear';
if (typeof options === 'object') {
duration = options.duration;
reversed = options.reversed === true;
selector = options.selector;
if (options.rotate === false) {
rorate = '';
}
else if (options.rotate === true) {
rorate = 'auto';
}
else if (options.rotate != null) {
rorate = `${options.rotate}`;
}
if (options.timing) {
timing = options.timing;
}
}
else {
duration = options;
reversed = false;
selector = null;
}
duration = duration || 1000;
const attrs = {
dur: `${duration}ms`,
repeatCount: '1',
calcMode: timing,
fill: 'freeze',
};
if (rorate) {
attrs.rotate = rorate;
}
if (reversed) {
attrs.keyPoints = '1;0';
attrs.keyTimes = '0;1';
}
if (typeof options === 'object') {
const { duration, reversed, selector, rotate, timing } = options, others = __rest(options, ["duration", "reversed", "selector", "rotate", "timing"]);
Object.keys(others).forEach((key) => {
attrs[key] = others[key];
});
}
let path;
if (typeof selector === 'string') {
path = this.findOne(selector, this.container, this.selectors);
}
else {
// Select connection path automatically.
path = this.containers.connection
? this.containers.connection
: this.container.querySelector('path');
}
if (!(path instanceof SVGPathElement)) {
throw new Error('Token animation requires a valid connection path.');
}
const target = typeof token === 'string' ? this.findOne(token) : token;
if (target == null) {
throw new Error('Token animation requires a valid token element.');
}
const parent = target.parentNode;
const revert = () => {
if (!parent) {
Dom.remove(target);
}
};
const vToken = Vector.create(target);
if (!parent) {
vToken.appendTo(this.graph.view.stage);
}
const onComplete = attrs.complete;
attrs.complete = (e) => {
revert();
if (callback) {
callback();
}
if (onComplete) {
onComplete(e);
}
};
const stop = vToken.animateAlongPath(attrs, path);
return () => {
revert();
stop();
};
}
// #endregion
getConnection() {
return this.path != null ? this.path.clone() : null;
}
getConnectionPathData() {
if (this.path == null) {
return '';
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'data')) {
cache.data = this.path.serialize();
}
return cache.data || '';
}
getConnectionSubdivisions() {
if (this.path == null) {
return null;
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'segmentSubdivisions')) {
cache.segmentSubdivisions = this.path.getSegmentSubdivisions();
}
return cache.segmentSubdivisions;
}
getConnectionLength() {
if (this.path == null) {
return 0;
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'length')) {
cache.length = this.path.length({
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
return cache.length;
}
getPointAtLength(length) {
if (this.path == null) {
return null;
}
return this.path.pointAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getPointAtRatio(ratio) {
if (this.path == null) {
return null;
}
if (NumberExt.isPercentage(ratio)) {
// eslint-disable-next-line
ratio = parseFloat(ratio) / 100;
}
return this.path.pointAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getTangentAtLength(length) {
if (this.path == null) {
return null;
}
return this.path.tangentAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getTangentAtRatio(ratio) {
if (this.path == null) {
return null;
}
return this.path.tangentAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPoint(point) {
if (this.path == null) {
return null;
}
return this.path.closestPoint(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPointLength(point) {
if (this.path == null) {
return null;
}
return this.path.closestPointLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPointRatio(point) {
if (this.path == null) {
return null;
}
return this.path.closestPointNormalizedLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getLabelPosition(x, y, p3, p4) {
const pos = { distance: 0 };
// normalize data from the two possible signatures
let angle = 0;
let options;
if (typeof p3 === 'number') {
angle = p3;
options = p4;
}
else {
options = p3;
}
if (options != null) {
pos.options = options;
}
// identify distance/offset settings
const isOffsetAbsolute = options && options.absoluteOffset;
const isDistanceRelative = !(options && options.absoluteDistance);
const isDistanceAbsoluteReverse = options && options.absoluteDistance && options.reverseDistance;
// find closest point t
const path = this.path;
const pathOptions = {
segmentSubdivisions: this.getConnectionSubdivisions(),
};
const labelPoint = new Point(x, y);
const t = path.closestPointT(labelPoint, pathOptions);
// distance
const totalLength = this.getConnectionLength() || 0;
let labelDistance = path.lengthAtT(t, pathOptions);
if (isDistanceRelative) {
labelDistance = totalLength > 0 ? labelDistance / totalLength : 0;
}
if (isDistanceAbsoluteReverse) {
// fix for end point (-0 => 1)
labelDistance = -1 * (totalLength - labelDistance) || 1;
}
pos.distance = labelDistance;
// offset
// use absolute offset if:
// - options.absoluteOffset is true,
// - options.absoluteOffset is not true but there is no tangent
let tangent;
if (!isOffsetAbsolute)
tangent = path.tangentAtT(t);
let labelOffset;
if (tangent) {
labelOffset = tangent.pointOffset(labelPoint);
}
else {
const closestPoint = path.pointAtT(t);
const labelOffsetDiff = labelPoint.diff(closestPoint);
labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y };
}
pos.offset = labelOffset;
pos.angle = angle;
return pos;
}
normalizeLabelPosition(pos) {
if (typeof pos === 'number') {
return { distance: pos };
}
return pos;
}
getLabelTransformationMatrix(labelPosition) {
const pos = this.normalizeLabelPosition(labelPosition);
const options = pos.options || {};
const labelAngle = pos.angle || 0;
const labelDistance = pos.distance;
const isDistanceRelative = labelDistance > 0 && labelDistance <= 1;
let labelOffset = 0;
const offsetCoord = { x: 0, y: 0 };
const offset = pos.offset;
if (offset) {
if (typeof offset === 'number') {
labelOffset = offset;
}
else {
if (offset.x != null) {
offsetCoord.x = offset.x;
}
if (offset.y != null) {
offsetCoord.y = offs