var util = require('../../../util');
|
|
var Label = require('./shared/Label').default;
|
|
var ComponentUtil = require('./shared/ComponentUtil').default;
|
|
var CubicBezierEdge = require('./edges/CubicBezierEdge').default;
|
|
var BezierEdgeDynamic = require('./edges/BezierEdgeDynamic').default;
|
|
var BezierEdgeStatic = require('./edges/BezierEdgeStatic').default;
|
|
var StraightEdge = require('./edges/StraightEdge').default;
|
|
|
|
|
|
/**
|
|
* An edge connects two nodes and has a specific direction.
|
|
*/
|
|
class Edge {
|
|
/**
|
|
* @param {Object} options values specific to this edge, must contain at least 'from' and 'to'
|
|
* @param {Object} body shared state from Network instance
|
|
* @param {Object} globalOptions options from the EdgesHandler instance
|
|
* @param {Object} defaultOptions default options from the EdgeHandler instance. Value and reference are constant
|
|
*/
|
|
constructor(options, body, globalOptions, defaultOptions) {
|
|
if (body === undefined) {
|
|
throw new Error("No body provided");
|
|
}
|
|
|
|
// Since globalOptions is constant in values as well as reference,
|
|
// Following needs to be done only once.
|
|
|
|
this.options = util.bridgeObject(globalOptions);
|
|
this.globalOptions = globalOptions;
|
|
this.defaultOptions = defaultOptions;
|
|
this.body = body;
|
|
|
|
// initialize variables
|
|
this.id = undefined;
|
|
this.fromId = undefined;
|
|
this.toId = undefined;
|
|
this.selected = false;
|
|
this.hover = false;
|
|
this.labelDirty = true;
|
|
|
|
this.baseWidth = this.options.width;
|
|
this.baseFontSize = this.options.font.size;
|
|
|
|
this.from = undefined; // a node
|
|
this.to = undefined; // a node
|
|
|
|
this.edgeType = undefined;
|
|
|
|
this.connected = false;
|
|
|
|
this.labelModule = new Label(this.body, this.options, true /* It's an edge label */);
|
|
this.setOptions(options);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or overwrite options for the edge
|
|
* @param {Object} options an object with options
|
|
* @returns {null|boolean} null if no options, boolean if date changed
|
|
*/
|
|
setOptions(options) {
|
|
if (!options) {
|
|
return;
|
|
}
|
|
|
|
Edge.parseOptions(this.options, options, true, this.globalOptions);
|
|
|
|
if (options.id !== undefined) {
|
|
this.id = options.id;
|
|
}
|
|
if (options.from !== undefined) {
|
|
this.fromId = options.from;
|
|
}
|
|
if (options.to !== undefined) {
|
|
this.toId = options.to;
|
|
}
|
|
if (options.title !== undefined) {
|
|
this.title = options.title;
|
|
}
|
|
if (options.value !== undefined) {
|
|
options.value = parseFloat(options.value);
|
|
}
|
|
|
|
let pile = [options, this.options, this.defaultOptions];
|
|
this.chooser = ComponentUtil.choosify('edge', pile);
|
|
|
|
// update label Module
|
|
this.updateLabelModule(options);
|
|
|
|
let dataChanged = this.updateEdgeType();
|
|
|
|
// if anything has been updates, reset the selection width and the hover width
|
|
this._setInteractionWidths();
|
|
|
|
// A node is connected when it has a from and to node that both exist in the network.body.nodes.
|
|
this.connect();
|
|
|
|
if (options.hidden !== undefined || options.physics !== undefined) {
|
|
dataChanged = true;
|
|
}
|
|
|
|
return dataChanged;
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @param {Object} parentOptions
|
|
* @param {Object} newOptions
|
|
* @param {boolean} [allowDeletion=false]
|
|
* @param {Object} [globalOptions={}]
|
|
* @param {boolean} [copyFromGlobals=false]
|
|
*/
|
|
static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}, copyFromGlobals = false) {
|
|
var fields = [
|
|
'arrowStrikethrough',
|
|
'id',
|
|
'from',
|
|
'hidden',
|
|
'hoverWidth',
|
|
'labelHighlightBold',
|
|
'length',
|
|
'line',
|
|
'opacity',
|
|
'physics',
|
|
'scaling',
|
|
'selectionWidth',
|
|
'selfReferenceSize',
|
|
'to',
|
|
'title',
|
|
'value',
|
|
'width',
|
|
'font',
|
|
'chosen',
|
|
'widthConstraint'
|
|
];
|
|
|
|
// only deep extend the items in the field array. These do not have shorthand.
|
|
util.selectiveDeepExtend(fields, parentOptions, newOptions, allowDeletion);
|
|
|
|
// Only copy label if it's a legal value.
|
|
if (ComponentUtil.isValidLabel(newOptions.label)) {
|
|
parentOptions.label = newOptions.label;
|
|
} else {
|
|
parentOptions.label = undefined;
|
|
}
|
|
|
|
util.mergeOptions(parentOptions, newOptions, 'smooth', globalOptions);
|
|
util.mergeOptions(parentOptions, newOptions, 'shadow', globalOptions);
|
|
|
|
if (newOptions.dashes !== undefined && newOptions.dashes !== null) {
|
|
parentOptions.dashes = newOptions.dashes;
|
|
}
|
|
else if (allowDeletion === true && newOptions.dashes === null) {
|
|
parentOptions.dashes = Object.create(globalOptions.dashes); // this sets the pointer of the option back to the global option.
|
|
}
|
|
|
|
// set the scaling newOptions
|
|
if (newOptions.scaling !== undefined && newOptions.scaling !== null) {
|
|
if (newOptions.scaling.min !== undefined) {parentOptions.scaling.min = newOptions.scaling.min;}
|
|
if (newOptions.scaling.max !== undefined) {parentOptions.scaling.max = newOptions.scaling.max;}
|
|
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', globalOptions.scaling);
|
|
}
|
|
else if (allowDeletion === true && newOptions.scaling === null) {
|
|
parentOptions.scaling = Object.create(globalOptions.scaling); // this sets the pointer of the option back to the global option.
|
|
}
|
|
|
|
// handle multiple input cases for arrows
|
|
if (newOptions.arrows !== undefined && newOptions.arrows !== null) {
|
|
if (typeof newOptions.arrows === 'string') {
|
|
let arrows = newOptions.arrows.toLowerCase();
|
|
parentOptions.arrows.to.enabled = arrows.indexOf("to") != -1;
|
|
parentOptions.arrows.middle.enabled = arrows.indexOf("middle") != -1;
|
|
parentOptions.arrows.from.enabled = arrows.indexOf("from") != -1;
|
|
}
|
|
else if (typeof newOptions.arrows === 'object') {
|
|
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'to', globalOptions.arrows);
|
|
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'middle', globalOptions.arrows);
|
|
util.mergeOptions(parentOptions.arrows, newOptions.arrows, 'from', globalOptions.arrows);
|
|
}
|
|
else {
|
|
throw new Error("The arrow newOptions can only be an object or a string. Refer to the documentation. You used:" + JSON.stringify(newOptions.arrows));
|
|
}
|
|
}
|
|
else if (allowDeletion === true && newOptions.arrows === null) {
|
|
parentOptions.arrows = Object.create(globalOptions.arrows); // this sets the pointer of the option back to the global option.
|
|
}
|
|
|
|
// handle multiple input cases for color
|
|
if (newOptions.color !== undefined && newOptions.color !== null) {
|
|
let fromColor = newOptions.color;
|
|
let toColor = parentOptions.color;
|
|
|
|
// If passed, fill in values from default options - required in the case of no prototype bridging
|
|
if (copyFromGlobals) {
|
|
util.deepExtend(toColor, globalOptions.color, false, allowDeletion);
|
|
} else {
|
|
// Clear local properties - need to do it like this in order to retain prototype bridges
|
|
for (var i in toColor) {
|
|
if (toColor.hasOwnProperty(i)) {
|
|
delete toColor[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (util.isString(toColor)) {
|
|
toColor.color = toColor;
|
|
toColor.highlight = toColor;
|
|
toColor.hover = toColor;
|
|
toColor.inherit = false;
|
|
if (fromColor.opacity === undefined) {
|
|
toColor.opacity = 1.0; // set default
|
|
}
|
|
}
|
|
else {
|
|
let colorsDefined = false;
|
|
if (fromColor.color !== undefined) {toColor.color = fromColor.color; colorsDefined = true;}
|
|
if (fromColor.highlight !== undefined) {toColor.highlight = fromColor.highlight; colorsDefined = true;}
|
|
if (fromColor.hover !== undefined) {toColor.hover = fromColor.hover; colorsDefined = true;}
|
|
if (fromColor.inherit !== undefined) {toColor.inherit = fromColor.inherit;}
|
|
if (fromColor.opacity !== undefined) {toColor.opacity = Math.min(1,Math.max(0,fromColor.opacity));}
|
|
|
|
if (colorsDefined === true) {
|
|
toColor.inherit = false;
|
|
} else {
|
|
if (toColor.inherit === undefined) {
|
|
toColor.inherit = 'from'; // Set default
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (allowDeletion === true && newOptions.color === null) {
|
|
parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
|
|
}
|
|
|
|
if (allowDeletion === true && newOptions.font === null) {
|
|
parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
*
|
|
* @returns {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}}
|
|
*/
|
|
getFormattingValues() {
|
|
let toArrow = (this.options.arrows.to === true) || (this.options.arrows.to.enabled === true)
|
|
let fromArrow = (this.options.arrows.from === true) || (this.options.arrows.from.enabled === true)
|
|
let middleArrow = (this.options.arrows.middle === true) || (this.options.arrows.middle.enabled === true)
|
|
let inheritsColor = this.options.color.inherit;
|
|
let values = {
|
|
toArrow: toArrow,
|
|
toArrowScale: this.options.arrows.to.scaleFactor,
|
|
toArrowType: this.options.arrows.to.type,
|
|
middleArrow: middleArrow,
|
|
middleArrowScale: this.options.arrows.middle.scaleFactor,
|
|
middleArrowType: this.options.arrows.middle.type,
|
|
fromArrow: fromArrow,
|
|
fromArrowScale: this.options.arrows.from.scaleFactor,
|
|
fromArrowType: this.options.arrows.from.type,
|
|
arrowStrikethrough: this.options.arrowStrikethrough,
|
|
color: (inheritsColor? undefined : this.options.color.color),
|
|
inheritsColor: inheritsColor,
|
|
opacity: this.options.color.opacity,
|
|
hidden: this.options.hidden,
|
|
length: this.options.length,
|
|
shadow: this.options.shadow.enabled,
|
|
shadowColor: this.options.shadow.color,
|
|
shadowSize: this.options.shadow.size,
|
|
shadowX: this.options.shadow.x,
|
|
shadowY: this.options.shadow.y,
|
|
dashes: this.options.dashes,
|
|
width: this.options.width
|
|
};
|
|
if (this.selected || this.hover) {
|
|
if (this.chooser === true) {
|
|
if (this.selected) {
|
|
let selectedWidth = this.options.selectionWidth;
|
|
if (typeof selectedWidth === 'function') {
|
|
values.width = selectedWidth(values.width);
|
|
} else if (typeof selectedWidth === 'number') {
|
|
values.width += selectedWidth;
|
|
}
|
|
values.width = Math.max(values.width, 0.3 / this.body.view.scale);
|
|
values.color = this.options.color.highlight;
|
|
values.shadow = this.options.shadow.enabled;
|
|
} else if (this.hover) {
|
|
let hoverWidth = this.options.hoverWidth;
|
|
if (typeof hoverWidth === 'function') {
|
|
values.width = hoverWidth(values.width);
|
|
} else if (typeof hoverWidth === 'number') {
|
|
values.width += hoverWidth;
|
|
}
|
|
values.width = Math.max(values.width, 0.3 / this.body.view.scale);
|
|
values.color = this.options.color.hover;
|
|
values.shadow = this.options.shadow.enabled;
|
|
}
|
|
} else if (typeof this.chooser === 'function') {
|
|
this.chooser(values, this.options.id, this.selected, this.hover);
|
|
if (values.color !== undefined) {
|
|
values.inheritsColor = false;
|
|
}
|
|
if (values.shadow === false) {
|
|
if ((values.shadowColor !== this.options.shadow.color) ||
|
|
(values.shadowSize !== this.options.shadow.size) ||
|
|
(values.shadowX !== this.options.shadow.x) ||
|
|
(values.shadowY !== this.options.shadow.y)) {
|
|
values.shadow = true;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
values.shadow = this.options.shadow.enabled;
|
|
values.width = Math.max(values.width, 0.3 / this.body.view.scale);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
/**
|
|
* update the options in the label module
|
|
*
|
|
* @param {Object} options
|
|
*/
|
|
updateLabelModule(options) {
|
|
let pile = [
|
|
options,
|
|
this.options,
|
|
this.globalOptions, // Currently set global edge options
|
|
this.defaultOptions
|
|
];
|
|
|
|
this.labelModule.update(this.options, pile);
|
|
|
|
if (this.labelModule.baseSize !== undefined) {
|
|
this.baseFontSize = this.labelModule.baseSize;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* update the edge type, set the options
|
|
* @returns {boolean}
|
|
*/
|
|
updateEdgeType() {
|
|
let smooth = this.options.smooth;
|
|
let dataChanged = false;
|
|
let changeInType = true;
|
|
if (this.edgeType !== undefined) {
|
|
if ((((this.edgeType instanceof BezierEdgeDynamic) &&
|
|
(smooth.enabled === true) &&
|
|
(smooth.type === 'dynamic'))) ||
|
|
(((this.edgeType instanceof CubicBezierEdge) &&
|
|
(smooth.enabled === true) &&
|
|
(smooth.type === 'cubicBezier'))) ||
|
|
(((this.edgeType instanceof BezierEdgeStatic) &&
|
|
(smooth.enabled === true) &&
|
|
(smooth.type !== 'dynamic') &&
|
|
(smooth.type !== 'cubicBezier'))) ||
|
|
(((this.edgeType instanceof StraightEdge) &&
|
|
(smooth.type.enabled === false)))) {
|
|
changeInType = false;
|
|
}
|
|
if (changeInType === true) {
|
|
dataChanged = this.cleanup();
|
|
}
|
|
}
|
|
if (changeInType === true) {
|
|
if (smooth.enabled === true) {
|
|
if (smooth.type === 'dynamic') {
|
|
dataChanged = true;
|
|
this.edgeType = new BezierEdgeDynamic(this.options, this.body, this.labelModule);
|
|
} else if (smooth.type === 'cubicBezier') {
|
|
this.edgeType = new CubicBezierEdge(this.options, this.body, this.labelModule);
|
|
} else {
|
|
this.edgeType = new BezierEdgeStatic(this.options, this.body, this.labelModule);
|
|
}
|
|
} else {
|
|
this.edgeType = new StraightEdge(this.options, this.body, this.labelModule);
|
|
}
|
|
} else { // if nothing changes, we just set the options.
|
|
this.edgeType.setOptions(this.options);
|
|
}
|
|
return dataChanged;
|
|
}
|
|
|
|
/**
|
|
* Connect an edge to its nodes
|
|
*/
|
|
connect() {
|
|
this.disconnect();
|
|
|
|
this.from = this.body.nodes[this.fromId] || undefined;
|
|
this.to = this.body.nodes[this.toId] || undefined;
|
|
this.connected = (this.from !== undefined && this.to !== undefined);
|
|
|
|
if (this.connected === true) {
|
|
this.from.attachEdge(this);
|
|
this.to.attachEdge(this);
|
|
}
|
|
else {
|
|
if (this.from) {
|
|
this.from.detachEdge(this);
|
|
}
|
|
if (this.to) {
|
|
this.to.detachEdge(this);
|
|
}
|
|
}
|
|
|
|
this.edgeType.connect();
|
|
}
|
|
|
|
|
|
/**
|
|
* Disconnect an edge from its nodes
|
|
*/
|
|
disconnect() {
|
|
if (this.from) {
|
|
this.from.detachEdge(this);
|
|
this.from = undefined;
|
|
}
|
|
if (this.to) {
|
|
this.to.detachEdge(this);
|
|
this.to = undefined;
|
|
}
|
|
|
|
this.connected = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* get the title of this edge.
|
|
* @return {string} title The title of the edge, or undefined when no title
|
|
* has been set.
|
|
*/
|
|
getTitle() {
|
|
return this.title;
|
|
}
|
|
|
|
|
|
/**
|
|
* check if this node is selecte
|
|
* @return {boolean} selected True if node is selected, else false
|
|
*/
|
|
isSelected() {
|
|
return this.selected;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieve the value of the edge. Can be undefined
|
|
* @return {number} value
|
|
*/
|
|
getValue() {
|
|
return this.options.value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Adjust the value range of the edge. The edge will adjust it's width
|
|
* based on its value.
|
|
* @param {number} min
|
|
* @param {number} max
|
|
* @param {number} total
|
|
*/
|
|
setValueRange(min, max, total) {
|
|
if (this.options.value !== undefined) {
|
|
var scale = this.options.scaling.customScalingFunction(min, max, total, this.options.value);
|
|
var widthDiff = this.options.scaling.max - this.options.scaling.min;
|
|
if (this.options.scaling.label.enabled === true) {
|
|
var fontDiff = this.options.scaling.label.max - this.options.scaling.label.min;
|
|
this.options.font.size = this.options.scaling.label.min + scale * fontDiff;
|
|
}
|
|
this.options.width = this.options.scaling.min + scale * widthDiff;
|
|
}
|
|
else {
|
|
this.options.width = this.baseWidth;
|
|
this.options.font.size = this.baseFontSize;
|
|
}
|
|
|
|
this._setInteractionWidths();
|
|
this.updateLabelModule();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @private
|
|
*/
|
|
_setInteractionWidths() {
|
|
if (typeof this.options.hoverWidth === 'function') {
|
|
this.edgeType.hoverWidth = this.options.hoverWidth(this.options.width);
|
|
} else {
|
|
this.edgeType.hoverWidth = this.options.hoverWidth + this.options.width;
|
|
}
|
|
if (typeof this.options.selectionWidth === 'function') {
|
|
this.edgeType.selectionWidth = this.options.selectionWidth(this.options.width);
|
|
} else {
|
|
this.edgeType.selectionWidth = this.options.selectionWidth + this.options.width;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Redraw a edge
|
|
* Draw this edge in the given canvas
|
|
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
*/
|
|
draw(ctx) {
|
|
let values = this.getFormattingValues();
|
|
if (values.hidden) {
|
|
return;
|
|
}
|
|
|
|
// get the via node from the edge type
|
|
let viaNode = this.edgeType.getViaNode();
|
|
let arrowData = {};
|
|
|
|
// restore edge targets to defaults
|
|
this.edgeType.fromPoint = this.edgeType.from;
|
|
this.edgeType.toPoint = this.edgeType.to;
|
|
|
|
// from and to arrows give a different end point for edges. we set them here
|
|
if (values.fromArrow) {
|
|
arrowData.from = this.edgeType.getArrowData(ctx, 'from', viaNode, this.selected, this.hover, values);
|
|
if (values.arrowStrikethrough === false)
|
|
this.edgeType.fromPoint = arrowData.from.core;
|
|
}
|
|
if (values.toArrow) {
|
|
arrowData.to = this.edgeType.getArrowData(ctx, 'to', viaNode, this.selected, this.hover, values);
|
|
if (values.arrowStrikethrough === false)
|
|
this.edgeType.toPoint = arrowData.to.core;
|
|
}
|
|
|
|
// the middle arrow depends on the line, which can depend on the to and from arrows so we do this one lastly.
|
|
if (values.middleArrow) {
|
|
arrowData.middle = this.edgeType.getArrowData(ctx,'middle', viaNode, this.selected, this.hover, values);
|
|
}
|
|
|
|
// draw everything
|
|
this.edgeType.drawLine(ctx, values, this.selected, this.hover, viaNode);
|
|
this.drawArrows(ctx, arrowData, values);
|
|
this.drawLabel(ctx, viaNode);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {Object} arrowData
|
|
* @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values
|
|
*/
|
|
drawArrows(ctx, arrowData, values) {
|
|
if (values.fromArrow) {
|
|
this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.from);
|
|
}
|
|
if (values.middleArrow) {
|
|
this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.middle);
|
|
}
|
|
if (values.toArrow) {
|
|
this.edgeType.drawArrowHead(ctx, values, this.selected, this.hover, arrowData.to);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {CanvasRenderingContext2D} ctx
|
|
* @param {Node} viaNode
|
|
*/
|
|
drawLabel(ctx, viaNode) {
|
|
if (this.options.label !== undefined) {
|
|
// set style
|
|
var node1 = this.from;
|
|
var node2 = this.to;
|
|
|
|
if (this.labelModule.differentState(this.selected, this.hover)) {
|
|
this.labelModule.getTextSize(ctx, this.selected, this.hover);
|
|
}
|
|
|
|
if (node1.id != node2.id) {
|
|
this.labelModule.pointToSelf = false;
|
|
var point = this.edgeType.getPoint(0.5, viaNode);
|
|
ctx.save();
|
|
|
|
let rotationPoint = this._getRotation(ctx);
|
|
if (rotationPoint.angle != 0) {
|
|
ctx.translate(rotationPoint.x, rotationPoint.y);
|
|
ctx.rotate(rotationPoint.angle);
|
|
}
|
|
|
|
// draw the label
|
|
this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
|
|
|
|
/*
|
|
// Useful debug code: draw a border around the label
|
|
// This should **not** be enabled in production!
|
|
var size = this.labelModule.getSize();; // ;; intentional so lint catches it
|
|
ctx.strokeStyle = "#ff0000";
|
|
ctx.strokeRect(size.left, size.top, size.width, size.height);
|
|
// End debug code
|
|
*/
|
|
|
|
ctx.restore();
|
|
}
|
|
else {
|
|
// Ignore the orientations.
|
|
this.labelModule.pointToSelf = true;
|
|
var x, y;
|
|
var radius = this.options.selfReferenceSize;
|
|
if (node1.shape.width > node1.shape.height) {
|
|
x = node1.x + node1.shape.width * 0.5;
|
|
y = node1.y - radius;
|
|
}
|
|
else {
|
|
x = node1.x + radius;
|
|
y = node1.y - node1.shape.height * 0.5;
|
|
}
|
|
point = this._pointOnCircle(x, y, radius, 0.125);
|
|
this.labelModule.draw(ctx, point.x, point.y, this.selected, this.hover);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine all visual elements of this edge instance, in which the given
|
|
* point falls within the bounding shape.
|
|
*
|
|
* @param {point} point
|
|
* @returns {Array.<edgeClickItem|edgeLabelClickItem>} list with the items which are on the point
|
|
*/
|
|
getItemsOnPoint(point) {
|
|
var ret = [];
|
|
|
|
if (this.labelModule.visible()) {
|
|
let rotationPoint = this._getRotation();
|
|
if (ComponentUtil.pointInRect(this.labelModule.getSize(), point, rotationPoint)) {
|
|
ret.push({edgeId:this.id, labelId:0});
|
|
}
|
|
}
|
|
|
|
let obj = {
|
|
left: point.x,
|
|
top: point.y
|
|
};
|
|
|
|
if (this.isOverlappingWith(obj)) {
|
|
ret.push({edgeId:this.id});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if this object is overlapping with the provided object
|
|
* @param {Object} obj an object with parameters left, top
|
|
* @return {boolean} True if location is located on the edge
|
|
*/
|
|
isOverlappingWith(obj) {
|
|
if (this.connected) {
|
|
var distMax = 10;
|
|
var xFrom = this.from.x;
|
|
var yFrom = this.from.y;
|
|
var xTo = this.to.x;
|
|
var yTo = this.to.y;
|
|
var xObj = obj.left;
|
|
var yObj = obj.top;
|
|
|
|
var dist = this.edgeType.getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj);
|
|
|
|
return (dist < distMax);
|
|
}
|
|
else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine the rotation point, if any.
|
|
*
|
|
* @param {CanvasRenderingContext2D} [ctx] if passed, do a recalculation of the label size
|
|
* @returns {rotationPoint} the point to rotate around and the angle in radians to rotate
|
|
* @private
|
|
*/
|
|
_getRotation(ctx) {
|
|
let viaNode = this.edgeType.getViaNode();
|
|
let point = this.edgeType.getPoint(0.5, viaNode);
|
|
|
|
if (ctx !== undefined) {
|
|
this.labelModule.calculateLabelSize(ctx, this.selected, this.hover, point.x, point.y);
|
|
}
|
|
|
|
let ret = {
|
|
x: point.x,
|
|
y: this.labelModule.size.yLine,
|
|
angle: 0
|
|
};
|
|
|
|
if (!this.labelModule.visible()) {
|
|
return ret; // Don't even bother doing the atan2, there's nothing to draw
|
|
}
|
|
|
|
if (this.options.font.align === "horizontal") {
|
|
return ret; // No need to calculate angle
|
|
}
|
|
|
|
var dy = this.from.y - this.to.y;
|
|
var dx = this.from.x - this.to.x;
|
|
var angle = Math.atan2(dy, dx); // radians
|
|
|
|
// rotate so that label is readable
|
|
if ((angle < -1 && dx < 0) || (angle > 0 && dx < 0)) {
|
|
angle += Math.PI;
|
|
}
|
|
ret.angle = angle;
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a point on a circle
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {number} radius
|
|
* @param {number} percentage Value between 0 (line start) and 1 (line end)
|
|
* @return {Object} point
|
|
* @private
|
|
*/
|
|
_pointOnCircle(x, y, radius, percentage) {
|
|
var angle = percentage * 2 * Math.PI;
|
|
return {
|
|
x: x + radius * Math.cos(angle),
|
|
y: y - radius * Math.sin(angle)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets selected state to true
|
|
*/
|
|
select() {
|
|
this.selected = true;
|
|
}
|
|
|
|
/**
|
|
* Sets selected state to false
|
|
*/
|
|
unselect() {
|
|
this.selected = false;
|
|
}
|
|
|
|
|
|
/**
|
|
* cleans all required things on delete
|
|
* @returns {*}
|
|
*/
|
|
cleanup() {
|
|
return this.edgeType.cleanup();
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove edge from the list and perform necessary cleanup.
|
|
*/
|
|
remove() {
|
|
this.cleanup();
|
|
this.disconnect();
|
|
delete this.body.edges[this.id];
|
|
}
|
|
|
|
|
|
/**
|
|
* Check if both connecting nodes exist
|
|
* @returns {boolean}
|
|
*/
|
|
endPointsValid() {
|
|
return this.body.nodes[this.fromId] !== undefined
|
|
&& this.body.nodes[this.toId] !== undefined;
|
|
}
|
|
}
|
|
|
|
export default Edge;
|