initial commit

This commit is contained in:
2026-03-22 03:21:45 +02:00
commit 897fea9f4e
15431 changed files with 2548840 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
/**
* Helper classes for LayoutEngine.
*
* Strategy pattern for usage of direction methods for hierarchical layouts.
*/
/**
* Interface definition for direction strategy classes.
*
* This class describes the interface for the Strategy
* pattern classes used to differentiate horizontal and vertical
* direction of hierarchical results.
*
* For a given direction, one coordinate will be 'fixed', meaning that it is
* determined by level.
* The other coordinate is 'unfixed', meaning that the nodes on a given level
* can still move along that coordinate. So:
*
* - `vertical` layout: `x` unfixed, `y` fixed per level
* - `horizontal` layout: `x` fixed per level, `y` unfixed
*
* The local methods are stubs and should be regarded as abstract.
* Derived classes **must** implement all the methods themselves.
*
* @private
*/
class DirectionInterface {
/** @ignore **/
abstract() {
throw new Error("Can't instantiate abstract class!");
}
/**
* This is a dummy call which is used to suppress the jsdoc errors of type:
*
* "'param' is assigned a value but never used"
*
* @ignore
**/
fake_use() {
// Do nothing special
}
/**
* Type to use to translate dynamic curves to, in the case of hierarchical layout.
* Dynamic curves do not work for these.
*
* The value should be perpendicular to the actual direction of the layout.
*
* @return {string} Direction, either 'vertical' or 'horizontal'
*/
curveType() { return this.abstract(); }
/**
* Return the value of the coordinate that is not fixed for this direction.
*
* @param {Node} node The node to read
* @return {number} Value of the unfixed coordinate
*/
getPosition(node) { this.fake_use(node); return this.abstract(); }
/**
* Set the value of the coordinate that is not fixed for this direction.
*
* @param {Node} node The node to adjust
* @param {number} position
* @param {number} [level] if specified, the hierarchy level that this node should be fixed to
*/
setPosition(node, position, level = undefined) { this.fake_use(node, position, level); this.abstract(); }
/**
* Get the width of a tree.
*
* A `tree` here is a subset of nodes within the network which are not connected to other nodes,
* only among themselves. In essence, it is a sub-network.
*
* @param {number} index The index number of a tree
* @return {number} the width of a tree in the view coordinates
*/
getTreeSize(index) { this.fake_use(index); return this.abstract(); }
/**
* Sort array of nodes on the unfixed coordinates.
*
* @param {Array.<Node>} nodeArray array of nodes to sort
*/
sort(nodeArray) { this.fake_use(nodeArray); this.abstract(); }
/**
* Assign the fixed coordinate of the node to the given level
*
* @param {Node} node The node to adjust
* @param {number} level The level to fix to
*/
fix(node, level) { this.fake_use(node, level); this.abstract(); }
/**
* Add an offset to the unfixed coordinate of the given node.
*
* @param {NodeId} nodeId Id of the node to adjust
* @param {number} diff Offset to add to the unfixed coordinate
*/
shift(nodeId, diff) { this.fake_use(nodeId, diff); this.abstract(); }
}
/**
* Vertical Strategy
*
* Coordinate `y` is fixed on levels, coordinate `x` is unfixed.
*
* @extends DirectionInterface
* @private
*/
class VerticalStrategy extends DirectionInterface {
/**
* Constructor
*
* @param {Object} layout reference to the parent LayoutEngine instance.
*/
constructor(layout) {
super();
this.layout = layout;
}
/** @inheritdoc */
curveType() {
return 'horizontal';
}
/** @inheritdoc */
getPosition(node) {
return node.x;
}
/** @inheritdoc */
setPosition(node, position, level = undefined) {
if (level !== undefined) {
this.layout.hierarchical.addToOrdering(node, level);
}
node.x = position;
}
/** @inheritdoc */
getTreeSize(index) {
let res = this.layout.hierarchical.getTreeSize(this.layout.body.nodes, index);
return {min: res.min_x, max: res.max_x};
}
/** @inheritdoc */
sort(nodeArray) {
nodeArray.sort(function(a, b) {
// Test on 'undefined' takes care of divergent behaviour in chrome
if (a.x === undefined || b.x === undefined) return 0; // THIS HAPPENS
return a.x - b.x;
});
}
/** @inheritdoc */
fix(node, level) {
node.y = this.layout.options.hierarchical.levelSeparation * level;
node.options.fixed.y = true;
}
/** @inheritdoc */
shift(nodeId, diff) {
this.layout.body.nodes[nodeId].x += diff;
}
}
/**
* Horizontal Strategy
*
* Coordinate `x` is fixed on levels, coordinate `y` is unfixed.
*
* @extends DirectionInterface
* @private
*/
class HorizontalStrategy extends DirectionInterface {
/**
* Constructor
*
* @param {Object} layout reference to the parent LayoutEngine instance.
*/
constructor(layout) {
super();
this.layout = layout;
}
/** @inheritdoc */
curveType() {
return 'vertical';
}
/** @inheritdoc */
getPosition(node) {
return node.y;
}
/** @inheritdoc */
setPosition(node, position, level = undefined) {
if (level !== undefined) {
this.layout.hierarchical.addToOrdering(node, level);
}
node.y = position;
}
/** @inheritdoc */
getTreeSize(index) {
let res = this.layout.hierarchical.getTreeSize(this.layout.body.nodes, index);
return {min: res.min_y, max: res.max_y};
}
/** @inheritdoc */
sort(nodeArray) {
nodeArray.sort(function(a, b) {
// Test on 'undefined' takes care of divergent behaviour in chrome
if (a.y === undefined || b.y === undefined) return 0; // THIS HAPPENS
return a.y - b.y;
});
}
/** @inheritdoc */
fix(node, level) {
node.x = this.layout.options.hierarchical.levelSeparation * level;
node.options.fixed.x = true;
}
/** @inheritdoc */
shift(nodeId, diff) {
this.layout.body.nodes[nodeId].y += diff;
}
}
export {HorizontalStrategy, VerticalStrategy};

781
node_modules/vis/lib/network/modules/components/Edge.js generated vendored Normal file
View File

@@ -0,0 +1,781 @@
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;

View File

@@ -0,0 +1,278 @@
var Hammer = require('../../../module/hammer');
var hammerUtil = require('../../../hammerUtil');
var keycharm = require('keycharm');
/**
* Navigation Handler
*/
class NavigationHandler {
/**
* @param {Object} body
* @param {Canvas} canvas
*/
constructor(body, canvas) {
this.body = body;
this.canvas = canvas;
this.iconsCreated = false;
this.navigationHammers = [];
this.boundFunctions = {};
this.touchTime = 0;
this.activated = false;
this.body.emitter.on("activate", () => {this.activated = true; this.configureKeyboardBindings();});
this.body.emitter.on("deactivate", () => {this.activated = false; this.configureKeyboardBindings();});
this.body.emitter.on("destroy", () => {if (this.keycharm !== undefined) {this.keycharm.destroy();}});
this.options = {}
}
/**
*
* @param {Object} options
*/
setOptions(options) {
if (options !== undefined) {
this.options = options;
this.create();
}
}
/**
* Creates or refreshes navigation and sets key bindings
*/
create() {
if (this.options.navigationButtons === true) {
if (this.iconsCreated === false) {
this.loadNavigationElements();
}
}
else if (this.iconsCreated === true) {
this.cleanNavigation();
}
this.configureKeyboardBindings();
}
/**
* Cleans up previous navigation items
*/
cleanNavigation() {
// clean hammer bindings
if (this.navigationHammers.length != 0) {
for (var i = 0; i < this.navigationHammers.length; i++) {
this.navigationHammers[i].destroy();
}
this.navigationHammers = [];
}
// clean up previous navigation items
if (this.navigationDOM && this.navigationDOM['wrapper'] && this.navigationDOM['wrapper'].parentNode) {
this.navigationDOM['wrapper'].parentNode.removeChild(this.navigationDOM['wrapper']);
}
this.iconsCreated = false;
}
/**
* Creation of the navigation controls nodes. They are drawn over the rest of the nodes and are not affected by scale and translation
* they have a triggerFunction which is called on click. If the position of the navigation controls is dependent
* on this.frame.canvas.clientWidth or this.frame.canvas.clientHeight, we flag horizontalAlignLeft and verticalAlignTop false.
* This means that the location will be corrected by the _relocateNavigation function on a size change of the canvas.
*
* @private
*/
loadNavigationElements() {
this.cleanNavigation();
this.navigationDOM = {};
var navigationDivs = ['up','down','left','right','zoomIn','zoomOut','zoomExtends'];
var navigationDivActions = ['_moveUp','_moveDown','_moveLeft','_moveRight','_zoomIn','_zoomOut','_fit'];
this.navigationDOM['wrapper'] = document.createElement('div');
this.navigationDOM['wrapper'].className = 'vis-navigation';
this.canvas.frame.appendChild(this.navigationDOM['wrapper']);
for (var i = 0; i < navigationDivs.length; i++) {
this.navigationDOM[navigationDivs[i]] = document.createElement('div');
this.navigationDOM[navigationDivs[i]].className = 'vis-button vis-' + navigationDivs[i];
this.navigationDOM['wrapper'].appendChild(this.navigationDOM[navigationDivs[i]]);
var hammer = new Hammer(this.navigationDOM[navigationDivs[i]]);
if (navigationDivActions[i] === "_fit") {
hammerUtil.onTouch(hammer, this._fit.bind(this));
}
else {
hammerUtil.onTouch(hammer, this.bindToRedraw.bind(this,navigationDivActions[i]));
}
this.navigationHammers.push(hammer);
}
// use a hammer for the release so we do not require the one used in the rest of the network
// the one the rest uses can be overloaded by the manipulation system.
var hammerFrame = new Hammer(this.canvas.frame);
hammerUtil.onRelease(hammerFrame, () => {this._stopMovement();});
this.navigationHammers.push(hammerFrame);
this.iconsCreated = true;
}
/**
*
* @param {string} action
*/
bindToRedraw(action) {
if (this.boundFunctions[action] === undefined) {
this.boundFunctions[action] = this[action].bind(this);
this.body.emitter.on("initRedraw", this.boundFunctions[action]);
this.body.emitter.emit("_startRendering");
}
}
/**
*
* @param {string} action
*/
unbindFromRedraw(action) {
if (this.boundFunctions[action] !== undefined) {
this.body.emitter.off("initRedraw", this.boundFunctions[action]);
this.body.emitter.emit("_stopRendering");
delete this.boundFunctions[action];
}
}
/**
* this stops all movement induced by the navigation buttons
*
* @private
*/
_fit() {
if (new Date().valueOf() - this.touchTime > 700) { // TODO: fix ugly hack to avoid hammer's double fireing of event (because we use release?)
this.body.emitter.emit("fit", {duration: 700});
this.touchTime = new Date().valueOf();
}
}
/**
* this stops all movement induced by the navigation buttons
*
* @private
*/
_stopMovement() {
for (let boundAction in this.boundFunctions) {
if (this.boundFunctions.hasOwnProperty(boundAction)) {
this.body.emitter.off("initRedraw", this.boundFunctions[boundAction]);
this.body.emitter.emit("_stopRendering");
}
}
this.boundFunctions = {};
}
/**
*
* @private
*/
_moveUp() {this.body.view.translation.y += this.options.keyboard.speed.y;}
/**
*
* @private
*/
_moveDown() {this.body.view.translation.y -= this.options.keyboard.speed.y;}
/**
*
* @private
*/
_moveLeft() {this.body.view.translation.x += this.options.keyboard.speed.x;}
/**
*
* @private
*/
_moveRight(){this.body.view.translation.x -= this.options.keyboard.speed.x;}
/**
*
* @private
*/
_zoomIn() {
var scaleOld = this.body.view.scale;
var scale = this.body.view.scale * (1 + this.options.keyboard.speed.zoom);
var translation = this.body.view.translation;
var scaleFrac = scale / scaleOld;
var tx = (1 - scaleFrac) * this.canvas.canvasViewCenter.x + translation.x * scaleFrac;
var ty = (1 - scaleFrac) * this.canvas.canvasViewCenter.y + translation.y * scaleFrac;
this.body.view.scale = scale;
this.body.view.translation = { x: tx, y: ty };
this.body.emitter.emit('zoom', { direction: '+', scale: this.body.view.scale, pointer: null });
}
/**
*
* @private
*/
_zoomOut() {
var scaleOld = this.body.view.scale;
var scale = this.body.view.scale / (1 + this.options.keyboard.speed.zoom);
var translation = this.body.view.translation;
var scaleFrac = scale / scaleOld;
var tx = (1 - scaleFrac) * this.canvas.canvasViewCenter.x + translation.x * scaleFrac;
var ty = (1 - scaleFrac) * this.canvas.canvasViewCenter.y + translation.y * scaleFrac;
this.body.view.scale = scale;
this.body.view.translation = { x: tx, y: ty };
this.body.emitter.emit('zoom', { direction: '-', scale: this.body.view.scale, pointer: null });
}
/**
* bind all keys using keycharm.
*/
configureKeyboardBindings() {
if (this.keycharm !== undefined) {
this.keycharm.destroy();
}
if (this.options.keyboard.enabled === true) {
if (this.options.keyboard.bindToWindow === true) {
this.keycharm = keycharm({container: window, preventDefault: true});
}
else {
this.keycharm = keycharm({container: this.canvas.frame, preventDefault: true});
}
this.keycharm.reset();
if (this.activated === true) {
this.keycharm.bind("up", () => {this.bindToRedraw("_moveUp") ;}, "keydown");
this.keycharm.bind("down", () => {this.bindToRedraw("_moveDown") ;}, "keydown");
this.keycharm.bind("left", () => {this.bindToRedraw("_moveLeft") ;}, "keydown");
this.keycharm.bind("right", () => {this.bindToRedraw("_moveRight");}, "keydown");
this.keycharm.bind("=", () => {this.bindToRedraw("_zoomIn") ;}, "keydown");
this.keycharm.bind("num+", () => {this.bindToRedraw("_zoomIn") ;}, "keydown");
this.keycharm.bind("num-", () => {this.bindToRedraw("_zoomOut") ;}, "keydown");
this.keycharm.bind("-", () => {this.bindToRedraw("_zoomOut") ;}, "keydown");
this.keycharm.bind("[", () => {this.bindToRedraw("_zoomOut") ;}, "keydown");
this.keycharm.bind("]", () => {this.bindToRedraw("_zoomIn") ;}, "keydown");
this.keycharm.bind("pageup", () => {this.bindToRedraw("_zoomIn") ;}, "keydown");
this.keycharm.bind("pagedown", () => {this.bindToRedraw("_zoomOut") ;}, "keydown");
this.keycharm.bind("up", () => {this.unbindFromRedraw("_moveUp") ;}, "keyup");
this.keycharm.bind("down", () => {this.unbindFromRedraw("_moveDown") ;}, "keyup");
this.keycharm.bind("left", () => {this.unbindFromRedraw("_moveLeft") ;}, "keyup");
this.keycharm.bind("right", () => {this.unbindFromRedraw("_moveRight");}, "keyup");
this.keycharm.bind("=", () => {this.unbindFromRedraw("_zoomIn") ;}, "keyup");
this.keycharm.bind("num+", () => {this.unbindFromRedraw("_zoomIn") ;}, "keyup");
this.keycharm.bind("num-", () => {this.unbindFromRedraw("_zoomOut") ;}, "keyup");
this.keycharm.bind("-", () => {this.unbindFromRedraw("_zoomOut") ;}, "keyup");
this.keycharm.bind("[", () => {this.unbindFromRedraw("_zoomOut") ;}, "keyup");
this.keycharm.bind("]", () => {this.unbindFromRedraw("_zoomIn") ;}, "keyup");
this.keycharm.bind("pageup", () => {this.unbindFromRedraw("_zoomIn") ;}, "keyup");
this.keycharm.bind("pagedown", () => {this.unbindFromRedraw("_zoomOut") ;}, "keyup");
}
}
}
}
export default NavigationHandler;

654
node_modules/vis/lib/network/modules/components/Node.js generated vendored Normal file
View File

@@ -0,0 +1,654 @@
var util = require('../../../util');
var Label = require('./shared/Label').default;
var ComponentUtil = require('./shared/ComponentUtil').default;
var Box = require('./nodes/shapes/Box').default;
var Circle = require('./nodes/shapes/Circle').default;
var CircularImage = require('./nodes/shapes/CircularImage').default;
var Database = require('./nodes/shapes/Database').default;
var Diamond = require('./nodes/shapes/Diamond').default;
var Dot = require('./nodes/shapes/Dot').default;
var Ellipse = require('./nodes/shapes/Ellipse').default;
var Icon = require('./nodes/shapes/Icon').default;
var Image = require('./nodes/shapes/Image').default;
var Square = require('./nodes/shapes/Square').default;
var Hexagon = require('./nodes/shapes/Hexagon').default;
var Star = require('./nodes/shapes/Star').default;
var Text = require('./nodes/shapes/Text').default;
var Triangle = require('./nodes/shapes/Triangle').default;
var TriangleDown = require('./nodes/shapes/TriangleDown').default;
var { printStyle } = require("../../../shared/Validator");
/**
* A node. A node can be connected to other nodes via one or multiple edges.
*/
class Node {
/**
*
* @param {object} options An object containing options for the node. All
* options are optional, except for the id.
* {number} id Id of the node. Required
* {string} label Text label for the node
* {number} x Horizontal position of the node
* {number} y Vertical position of the node
* {string} shape Node shape
* {string} image An image url
* {string} title A title text, can be HTML
* {anytype} group A group name or number
*
* @param {Object} body Shared state of current network instance
* @param {Network.Images} imagelist A list with images. Only needed when the node has an image
* @param {Groups} grouplist A list with groups. Needed for retrieving group options
* @param {Object} globalOptions Current global node options; these serve as defaults for the node instance
* @param {Object} defaultOptions Global default options for nodes; note that this is also the prototype
* for parameter `globalOptions`.
*/
constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions) {
this.options = util.bridgeObject(globalOptions);
this.globalOptions = globalOptions;
this.defaultOptions = defaultOptions;
this.body = body;
this.edges = []; // all edges connected to this node
// set defaults for the options
this.id = undefined;
this.imagelist = imagelist;
this.grouplist = grouplist;
// state options
this.x = undefined;
this.y = undefined;
this.baseSize = this.options.size;
this.baseFontSize = this.options.font.size;
this.predefinedPosition = false; // used to check if initial fit should just take the range or approximate
this.selected = false;
this.hover = false;
this.labelModule = new Label(this.body, this.options, false /* Not edge label */);
this.setOptions(options);
}
/**
* Attach a edge to the node
* @param {Edge} edge
*/
attachEdge(edge) {
if (this.edges.indexOf(edge) === -1) {
this.edges.push(edge);
}
}
/**
* Detach a edge from the node
*
* @param {Edge} edge
*/
detachEdge(edge) {
var index = this.edges.indexOf(edge);
if (index != -1) {
this.edges.splice(index, 1);
}
}
/**
* Set or overwrite options for the node
*
* @param {Object} options an object with options
* @returns {null|boolean}
*/
setOptions(options) {
let currentShape = this.options.shape;
if (!options) {
return; // Note that the return value will be 'undefined'! This is OK.
}
// basic options
if (options.id !== undefined) {this.id = options.id;}
if (this.id === undefined) {
throw new Error("Node must have an id");
}
Node.checkMass(options, this.id);
// set these options locally
// clear x and y positions
if (options.x !== undefined) {
if (options.x === null) {this.x = undefined; this.predefinedPosition = false;}
else {this.x = parseInt(options.x); this.predefinedPosition = true;}
}
if (options.y !== undefined) {
if (options.y === null) {this.y = undefined; this.predefinedPosition = false;}
else {this.y = parseInt(options.y); this.predefinedPosition = true;}
}
if (options.size !== undefined) {this.baseSize = options.size;}
if (options.value !== undefined) {options.value = parseFloat(options.value);}
// this transforms all shorthands into fully defined options
Node.parseOptions(this.options, options, true, this.globalOptions, this.grouplist);
let pile = [options, this.options, this.defaultOptions];
this.chooser = ComponentUtil.choosify('node', pile);
this._load_images();
this.updateLabelModule(options);
this.updateShape(currentShape);
return (options.hidden !== undefined || options.physics !== undefined);
}
/**
* Load the images from the options, for the nodes that need them.
*
* TODO: The imageObj members should be moved to CircularImageBase.
* It's the only place where they are required.
*
* @private
*/
_load_images() {
// Don't bother loading for nodes without images
if (this.options.shape !== 'circularImage' && this.options.shape !== 'image') {
return;
}
if (this.options.image === undefined) {
throw new Error("Option image must be defined for node type '" + this.options.shape + "'");
}
if (this.imagelist === undefined) {
throw new Error("Internal Error: No images provided");
}
if (typeof this.options.image === 'string') {
this.imageObj = this.imagelist.load(this.options.image, this.options.brokenImage, this.id);
} else {
if (this.options.image.unselected === undefined) {
throw new Error("No unselected image provided");
}
this.imageObj = this.imagelist.load(this.options.image.unselected, this.options.brokenImage, this.id);
if (this.options.image.selected !== undefined) {
this.imageObjAlt = this.imagelist.load(this.options.image.selected, this.options.brokenImage, this.id);
} else {
this.imageObjAlt = undefined;
}
}
}
/**
* Copy group option values into the node options.
*
* The group options override the global node options, so the copy of group options
* must happen *after* the global node options have been set.
*
* This method must also be called also if the global node options have changed and the group options did not.
*
* @param {Object} parentOptions
* @param {Object} newOptions new values for the options, currently only passed in for check
* @param {Object} groupList
*/
static updateGroupOptions(parentOptions, newOptions, groupList) {
if (groupList === undefined) return; // No groups, nothing to do
var group = parentOptions.group;
// paranoia: the selected group is already merged into node options, check.
if (newOptions !== undefined && newOptions.group !== undefined && group !== newOptions.group) {
throw new Error("updateGroupOptions: group values in options don't match.");
}
var hasGroup = (typeof group === 'number' || (typeof group === 'string' && group != ''));
if (!hasGroup) return; // current node has no group, no need to merge
var groupObj = groupList.get(group);
// Skip merging of group font options into parent; these are required to be distinct for labels
// TODO: It might not be a good idea either to merge the rest of the options, investigate this.
util.selectiveNotDeepExtend(['font'], parentOptions, groupObj);
// the color object needs to be completely defined.
// Since groups can partially overwrite the colors, we parse it again, just in case.
parentOptions.color = util.parseColor(parentOptions.color);
}
/**
* This process all possible shorthands in the new options and makes sure that the parentOptions are fully defined.
* Static so it can also be used by the handler.
*
* @param {Object} parentOptions
* @param {Object} newOptions
* @param {boolean} [allowDeletion=false]
* @param {Object} [globalOptions={}]
* @param {Object} [groupList]
* @static
*/
static parseOptions(parentOptions, newOptions, allowDeletion = false, globalOptions = {}, groupList) {
var fields = [
'color',
'fixed',
'shadow'
];
util.selectiveNotDeepExtend(fields, parentOptions, newOptions, allowDeletion);
Node.checkMass(newOptions);
// merge the shadow options into the parent.
util.mergeOptions(parentOptions, newOptions, 'shadow', globalOptions);
// individual shape newOptions
if (newOptions.color !== undefined && newOptions.color !== null) {
let parsedColor = util.parseColor(newOptions.color);
util.fillIfDefined(parentOptions.color, parsedColor);
}
else if (allowDeletion === true && newOptions.color === null) {
parentOptions.color = util.bridgeObject(globalOptions.color); // set the object back to the global options
}
// handle the fixed options
if (newOptions.fixed !== undefined && newOptions.fixed !== null) {
if (typeof newOptions.fixed === 'boolean') {
parentOptions.fixed.x = newOptions.fixed;
parentOptions.fixed.y = newOptions.fixed;
}
else {
if (newOptions.fixed.x !== undefined && typeof newOptions.fixed.x === 'boolean') {
parentOptions.fixed.x = newOptions.fixed.x;
}
if (newOptions.fixed.y !== undefined && typeof newOptions.fixed.y === 'boolean') {
parentOptions.fixed.y = newOptions.fixed.y;
}
}
}
if (allowDeletion === true && newOptions.font === null) {
parentOptions.font = util.bridgeObject(globalOptions.font); // set the object back to the global options
}
Node.updateGroupOptions(parentOptions, newOptions, groupList);
// handle the scaling options, specifically the label part
if (newOptions.scaling !== undefined) {
util.mergeOptions(parentOptions.scaling, newOptions.scaling, 'label', globalOptions.scaling);
}
}
/**
*
* @returns {{color: *, borderWidth: *, borderColor: *, size: *, borderDashes: (boolean|Array|allOptions.nodes.shapeProperties.borderDashes|{boolean, array}), borderRadius: (number|allOptions.nodes.shapeProperties.borderRadius|{number}|Array), shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *}}
*/
getFormattingValues() {
let values = {
color: this.options.color.background,
borderWidth: this.options.borderWidth,
borderColor: this.options.color.border,
size: this.options.size,
borderDashes: this.options.shapeProperties.borderDashes,
borderRadius: this.options.shapeProperties.borderRadius,
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
};
if (this.selected || this.hover) {
if (this.chooser === true) {
if (this.selected) {
values.borderWidth *= 2;
values.color = this.options.color.highlight.background;
values.borderColor = this.options.color.highlight.border;
values.shadow = this.options.shadow.enabled;
} else if (this.hover) {
values.color = this.options.color.hover.background;
values.borderColor = this.options.color.hover.border;
values.shadow = this.options.shadow.enabled;
}
} else if (typeof this.chooser === 'function') {
this.chooser(values, this.options.id, this.selected, this.hover);
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;
}
return values;
}
/**
*
* @param {Object} options
*/
updateLabelModule(options) {
if (this.options.label === undefined || this.options.label === null) {
this.options.label = '';
}
Node.updateGroupOptions(this.options, options, this.grouplist);
//
// Note:The prototype chain for this.options is:
//
// this.options -> NodesHandler.options -> NodesHandler.defaultOptions
// (also: this.globalOptions)
//
// Note that the prototypes are mentioned explicitly in the pile list below;
// WE DON'T WANT THE ORDER OF THE PROTOTYPES!!!! At least, not for font handling of labels.
// This is a good indication that the prototype usage of options is deficient.
//
var currentGroup = this.grouplist.get(this.options.group, false);
let pile = [
options, // new options
this.options, // current node options, see comment above for prototype
currentGroup, // group options, if any
this.globalOptions, // Currently set global node options
this.defaultOptions // Default global node options
];
this.labelModule.update(this.options, pile);
if (this.labelModule.baseSize !== undefined) {
this.baseFontSize = this.labelModule.baseSize;
}
}
/**
*
* @param {string} currentShape
*/
updateShape(currentShape) {
if (currentShape === this.options.shape && this.shape) {
this.shape.setOptions(this.options, this.imageObj, this.imageObjAlt);
}
else {
// choose draw method depending on the shape
switch (this.options.shape) {
case 'box':
this.shape = new Box(this.options, this.body, this.labelModule);
break;
case 'circle':
this.shape = new Circle(this.options, this.body, this.labelModule);
break;
case 'circularImage':
this.shape = new CircularImage(this.options, this.body, this.labelModule, this.imageObj, this.imageObjAlt);
break;
case 'database':
this.shape = new Database(this.options, this.body, this.labelModule);
break;
case 'diamond':
this.shape = new Diamond(this.options, this.body, this.labelModule);
break;
case 'dot':
this.shape = new Dot(this.options, this.body, this.labelModule);
break;
case 'ellipse':
this.shape = new Ellipse(this.options, this.body, this.labelModule);
break;
case 'icon':
this.shape = new Icon(this.options, this.body, this.labelModule);
break;
case 'image':
this.shape = new Image(this.options, this.body, this.labelModule, this.imageObj, this.imageObjAlt);
break;
case 'square':
this.shape = new Square(this.options, this.body, this.labelModule);
break;
case 'hexagon':
this.shape = new Hexagon(this.options, this.body, this.labelModule);
break;
case 'star':
this.shape = new Star(this.options, this.body, this.labelModule);
break;
case 'text':
this.shape = new Text(this.options, this.body, this.labelModule);
break;
case 'triangle':
this.shape = new Triangle(this.options, this.body, this.labelModule);
break;
case 'triangleDown':
this.shape = new TriangleDown(this.options, this.body, this.labelModule);
break;
default:
this.shape = new Ellipse(this.options, this.body, this.labelModule);
break;
}
}
this.needsRefresh();
}
/**
* select this node
*/
select() {
this.selected = true;
this.needsRefresh();
}
/**
* unselect this node
*/
unselect() {
this.selected = false;
this.needsRefresh();
}
/**
* Reset the calculated size of the node, forces it to recalculate its size
*/
needsRefresh() {
this.shape.refreshNeeded = true;
}
/**
* get the title of this node.
* @return {string} title The title of the node, or undefined when no title
* has been set.
*/
getTitle() {
return this.options.title;
}
/**
* Calculate the distance to the border of the Node
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle Angle in radians
* @returns {number} distance Distance to the border in pixels
*/
distanceToBorder(ctx, angle) {
return this.shape.distanceToBorder(ctx,angle);
}
/**
* Check if this node has a fixed x and y position
* @return {boolean} true if fixed, false if not
*/
isFixed() {
return (this.options.fixed.x && this.options.fixed.y);
}
/**
* 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 node. Can be undefined
* @return {number} value
*/
getValue() {
return this.options.value;
}
/**
* Get the current dimensions of the label
*
* @return {rect}
*/
getLabelSize() {
return this.labelModule.size();
}
/**
* Adjust the value range of the node. The node will adjust it's size
* 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 sizeDiff = 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.size = this.options.scaling.min + scale * sizeDiff;
}
else {
this.options.size = this.baseSize;
this.options.font.size = this.baseFontSize;
}
this.updateLabelModule();
}
/**
* Draw this node 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();
this.shape.draw(ctx, this.x, this.y, this.selected, this.hover, values);
}
/**
* Update the bounding box of the shape
* @param {CanvasRenderingContext2D} ctx
*/
updateBoundingBox(ctx) {
this.shape.updateBoundingBox(this.x,this.y,ctx);
}
/**
* Recalculate the size of this node in the given canvas
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
* @param {CanvasRenderingContext2D} ctx
*/
resize(ctx) {
let values = this.getFormattingValues();
this.shape.resize(ctx, this.selected, this.hover, values);
}
/**
* Determine all visual elements of this node instance, in which the given
* point falls within the bounding shape.
*
* @param {point} point
* @returns {Array.<nodeClickItem|nodeLabelClickItem>} list with the items which are on the point
*/
getItemsOnPoint(point) {
var ret = [];
if (this.labelModule.visible()) {
if (ComponentUtil.pointInRect(this.labelModule.getSize(), point)) {
ret.push({nodeId:this.id, labelId:0});
}
}
if (ComponentUtil.pointInRect(this.shape.boundingBox, point)) {
ret.push({nodeId:this.id});
}
return ret;
}
/**
* Check if this object is overlapping with the provided object
* @param {Object} obj an object with parameters left, top, right, bottom
* @return {boolean} True if location is located on node
*/
isOverlappingWith(obj) {
return (
this.shape.left < obj.right &&
this.shape.left + this.shape.width > obj.left &&
this.shape.top < obj.bottom &&
this.shape.top + this.shape.height > obj.top
);
}
/**
* Check if this object is overlapping with the provided object
* @param {Object} obj an object with parameters left, top, right, bottom
* @return {boolean} True if location is located on node
*/
isBoundingBoxOverlappingWith(obj) {
return (
this.shape.boundingBox.left < obj.right &&
this.shape.boundingBox.right > obj.left &&
this.shape.boundingBox.top < obj.bottom &&
this.shape.boundingBox.bottom > obj.top
);
}
/**
* Check valid values for mass
*
* The mass may not be negative or zero. If it is, reset to 1
*
* @param {object} options
* @param {Node.id} id
* @static
*/
static checkMass(options, id) {
if (options.mass !== undefined && options.mass <= 0) {
let strId = '';
if (id !== undefined) {
strId = ' in node id: ' + id;
}
console.log('%cNegative or zero mass disallowed' + strId +
', setting mass to 1.' , printStyle);
options.mass = 1;
}
}
}
export default Node;

View File

@@ -0,0 +1,68 @@
/**
* The FloydWarshall algorithm is an algorithm for finding shortest paths in
* a weighted graph with positive or negative edge weights (but with no negative
* cycles). - https://en.wikipedia.org/wiki/FloydWarshall_algorithm
*/
class FloydWarshall {
/**
* @ignore
*/
constructor() {
}
/**
*
* @param {Object} body
* @param {Array.<Node>} nodesArray
* @param {Array.<Edge>} edgesArray
* @returns {{}}
*/
getDistances(body, nodesArray, edgesArray) {
let D_matrix = {};
let edges = body.edges;
// prepare matrix with large numbers
for (let i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i];
let cell = {};
D_matrix[node] = cell;
for (let j = 0; j < nodesArray.length; j++) {
cell[nodesArray[j]] = (i == j ? 0 : 1e9);
}
}
// put the weights for the edges in. This assumes unidirectionality.
for (let i = 0; i < edgesArray.length; i++) {
let edge = edges[edgesArray[i]];
// edge has to be connected if it counts to the distances. If it is connected to inner clusters it will crash so we also check if it is in the D_matrix
if (edge.connected === true && D_matrix[edge.fromId] !== undefined && D_matrix[edge.toId] !== undefined) {
D_matrix[edge.fromId][edge.toId] = 1;
D_matrix[edge.toId][edge.fromId] = 1;
}
}
let nodeCount = nodesArray.length;
// Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
for (let k = 0; k < nodeCount; k++) {
let knode = nodesArray[k];
let kcolm = D_matrix[knode];
for (let i = 0; i < nodeCount - 1; i++) {
let inode = nodesArray[i];
let icolm = D_matrix[inode];
for (let j = i + 1; j < nodeCount; j++) {
let jnode = nodesArray[j];
let jcolm = D_matrix[jnode];
let val = Math.min(icolm[jnode], icolm[knode] + kcolm[jnode]);
icolm[jnode] = val;
jcolm[inode] = val;
}
}
}
return D_matrix;
}
}
export default FloydWarshall;

View File

@@ -0,0 +1,194 @@
import BezierEdgeBase from './util/BezierEdgeBase'
/**
* A Dynamic Bezier Edge. Bezier curves are used to model smooth gradual
* curves in paths between nodes. The Dynamic piece refers to how the curve
* reacts to physics changes.
*
* @extends BezierEdgeBase
*/
class BezierEdgeDynamic extends BezierEdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
//this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked.
super(options, body, labelModule); // --> this calls the setOptions below
this._boundFunction = () => {this.positionBezierNode();};
this.body.emitter.on("_repositionBezierNodes", this._boundFunction);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
// check if the physics has changed.
let physicsChange = false;
if (this.options.physics !== options.physics) {
physicsChange = true;
}
// set the options and the to and from nodes
this.options = options;
this.id = this.options.id;
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
// setup the support node and connect
this.setupSupportNode();
this.connect();
// when we change the physics state of the edge, we reposition the support node.
if (physicsChange === true) {
this.via.setOptions({physics: this.options.physics});
this.positionBezierNode();
}
}
/**
* Connects an edge to node(s)
*/
connect() {
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
if (this.from === undefined || this.to === undefined || this.options.physics === false) {
this.via.setOptions({physics:false})
}
else {
// fix weird behaviour where a self referencing node has physics enabled
if (this.from.id === this.to.id) {
this.via.setOptions({physics: false})
}
else {
this.via.setOptions({physics: true})
}
}
}
/**
* remove the support nodes
* @returns {boolean}
*/
cleanup() {
this.body.emitter.off("_repositionBezierNodes", this._boundFunction);
if (this.via !== undefined) {
delete this.body.nodes[this.via.id];
this.via = undefined;
return true;
}
return false;
}
/**
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
* are used for the force calculation.
*
* The changed data is not called, if needed, it is returned by the main edge constructor.
* @private
*/
setupSupportNode() {
if (this.via === undefined) {
var nodeId = "edgeId:" + this.id;
var node = this.body.functions.createNode({
id: nodeId,
shape: 'circle',
physics:true,
hidden:true
});
this.body.nodes[nodeId] = node;
this.via = node;
this.via.parentEdgeId = this.id;
this.positionBezierNode();
}
}
/**
* Positions bezier node
*/
positionBezierNode() {
if (this.via !== undefined && this.from !== undefined && this.to !== undefined) {
this.via.x = 0.5 * (this.from.x + this.to.x);
this.via.y = 0.5 * (this.from.y + this.to.y);
}
else if (this.via !== undefined) {
this.via.x = 0;
this.via.y = 0;
}
}
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
* @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
* @param {Node} viaNode
* @private
*/
_line(ctx, values, viaNode) {
this._bezierCurve(ctx, values, viaNode);
}
/**
*
* @returns {Node|undefined|*|{index, line, column}}
*/
getViaNode() {
return this.via;
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
*
* @param {number} percentage
* @param {Node} viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, viaNode = this.via) {
let t = percentage;
let x, y;
if (this.from === this.to){
let [cx,cy,cr] = this._getCircleData(this.from);
let a = 2 * Math.PI * (1 - t);
x = cx + cr * Math.sin(a);
y = cy + cr - cr * (1 - Math.cos(a));
} else {
x = Math.pow(1 - t, 2) * this.fromPoint.x + 2 * t * (1 - t) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
y = Math.pow(1 - t, 2) * this.fromPoint.y + 2 * t * (1 - t) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
}
return {x: x, y: y};
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @returns {*}
* @private
*/
_findBorderPosition(nearNode, ctx) {
return this._findBorderPositionBezier(nearNode, ctx, this.via);
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @returns {number}
* @private
*/
_getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, this.via);
}
}
export default BezierEdgeDynamic;

View File

@@ -0,0 +1,207 @@
import BezierEdgeBase from './util/BezierEdgeBase'
/**
* A Static Bezier Edge. Bezier curves are used to model smooth gradual
* curves in paths between nodes.
*
* @extends BezierEdgeBase
*/
class BezierEdgeStatic extends BezierEdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
* @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
* @param {Node} viaNode
* @private
*/
_line(ctx, values, viaNode) {
this._bezierCurve(ctx, values, viaNode);
}
/**
*
* @returns {Array.<{x: number, y: number}>}
*/
getViaNode() {
return this._getViaCoordinates();
}
/**
* We do not use the to and fromPoints here to make the via nodes the same as edges without arrows.
* @returns {{x: undefined, y: undefined}}
* @private
*/
_getViaCoordinates() {
// Assumption: x/y coordinates in from/to always defined
let xVia = undefined;
let yVia = undefined;
let factor = this.options.smooth.roundness;
let type = this.options.smooth.type;
let dx = Math.abs(this.from.x - this.to.x);
let dy = Math.abs(this.from.y - this.to.y);
if (type === 'discrete' || type === 'diagonalCross') {
let stepX;
let stepY;
if (dx <= dy) {
stepX = stepY = factor * dy;
} else {
stepX = stepY = factor * dx;
}
if (this.from.x > this.to.x) stepX = -stepX;
if (this.from.y >= this.to.y) stepY = -stepY;
xVia = this.from.x + stepX;
yVia = this.from.y + stepY;
if (type === "discrete") {
if (dx <= dy) {
xVia = dx < factor * dy ? this.from.x : xVia;
} else {
yVia = dy < factor * dx ? this.from.y : yVia;
}
}
}
else if (type === "straightCross") {
let stepX = (1 - factor) * dx;
let stepY = (1 - factor) * dy;
if (dx <= dy) { // up - down
stepX = 0;
if (this.from.y < this.to.y) stepY = -stepY;
}
else { // left - right
if (this.from.x < this.to.x) stepX = -stepX;
stepY = 0;
}
xVia = this.to.x + stepX;
yVia = this.to.y + stepY;
}
else if (type === 'horizontal') {
let stepX = (1 - factor) * dx;
if (this.from.x < this.to.x) stepX = -stepX;
xVia = this.to.x + stepX;
yVia = this.from.y;
}
else if (type === 'vertical') {
let stepY = (1 - factor) * dy;
if (this.from.y < this.to.y) stepY = -stepY;
xVia = this.from.x;
yVia = this.to.y + stepY;
}
else if (type === 'curvedCW') {
dx = this.to.x - this.from.x;
dy = this.from.y - this.to.y;
let radius = Math.sqrt(dx * dx + dy * dy);
let pi = Math.PI;
let originalAngle = Math.atan2(dy, dx);
let myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi);
xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
}
else if (type === 'curvedCCW') {
dx = this.to.x - this.from.x;
dy = this.from.y - this.to.y;
let radius = Math.sqrt(dx * dx + dy * dy);
let pi = Math.PI;
let originalAngle = Math.atan2(dy, dx);
let myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi);
xVia = this.from.x + (factor * 0.5 + 0.5) * radius * Math.sin(myAngle);
yVia = this.from.y + (factor * 0.5 + 0.5) * radius * Math.cos(myAngle);
}
else { // continuous
let stepX;
let stepY;
if (dx <= dy) {
stepX = stepY = factor * dy;
} else {
stepX = stepY = factor * dx;
}
if (this.from.x > this.to.x) stepX = -stepX;
if (this.from.y >= this.to.y) stepY = -stepY;
xVia = this.from.x + stepX;
yVia = this.from.y + stepY;
if (dx <= dy) {
if (this.from.x <= this.to.x) {
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else {
xVia = this.to.x > xVia ? this.to.x : xVia;
}
}
else {
if (this.from.y >= this.to.y) {
yVia = this.to.y > yVia ? this.to.y : yVia;
} else {
yVia = this.to.y < yVia ? this.to.y : yVia;
}
}
}
return {x: xVia, y: yVia};
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {*}
* @private
*/
_findBorderPosition(nearNode, ctx, options = {}) {
return this._findBorderPositionBezier(nearNode, ctx, options.via);
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @param {Node} viaNode
* @returns {number}
* @private
*/
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, viaNode = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, viaNode);
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param {number} percentage
* @param {Node} viaNode
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, viaNode = this._getViaCoordinates()) {
var t = percentage;
var x = Math.pow(1 - t, 2) * this.fromPoint.x + (2 * t * (1 - t)) * viaNode.x + Math.pow(t, 2) * this.toPoint.x;
var y = Math.pow(1 - t, 2) * this.fromPoint.y + (2 * t * (1 - t)) * viaNode.y + Math.pow(t, 2) * this.toPoint.y;
return {x: x, y: y};
}
}
export default BezierEdgeStatic;

View File

@@ -0,0 +1,121 @@
import CubicBezierEdgeBase from './util/CubicBezierEdgeBase'
/**
* A Cubic Bezier Edge. Bezier curves are used to model smooth gradual
* curves in paths between nodes.
*
* @extends CubicBezierEdgeBase
*/
class CubicBezierEdge extends CubicBezierEdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
* @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
* @param {Array.<Node>} viaNodes
* @private
*/
_line(ctx, values, viaNodes) {
// get the coordinates of the support points.
let via1 = viaNodes[0];
let via2 = viaNodes[1];
this._bezierCurve(ctx, values, via1, via2);
}
/**
*
* @returns {Array.<{x: number, y: number}>}
* @private
*/
_getViaCoordinates() {
let dx = this.from.x - this.to.x;
let dy = this.from.y - this.to.y;
let x1, y1, x2, y2;
let roundness = this.options.smooth.roundness;
// horizontal if x > y or if direction is forced or if direction is horizontal
if ((Math.abs(dx) > Math.abs(dy) || this.options.smooth.forceDirection === true || this.options.smooth.forceDirection === 'horizontal') && this.options.smooth.forceDirection !== 'vertical') {
y1 = this.from.y;
y2 = this.to.y;
x1 = this.from.x - roundness * dx;
x2 = this.to.x + roundness * dx;
}
else {
y1 = this.from.y - roundness * dy;
y2 = this.to.y + roundness * dy;
x1 = this.from.x;
x2 = this.to.x;
}
return [{x: x1, y: y1},{x: x2, y: y2}];
}
/**
*
* @returns {Array.<{x: number, y: number}>}
*/
getViaNode() {
return this._getViaCoordinates();
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @returns {{x: number, y: number, t: number}}
* @private
*/
_findBorderPosition(nearNode, ctx) {
return this._findBorderPositionBezier(nearNode, ctx);
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @param {Node} via1
* @param {Node} via2
* @returns {number}
* @private
*/
_getDistanceToEdge(x1, y1, x2, y2, x3, y3, [via1, via2] = this._getViaCoordinates()) { // x3,y3 is the point
return this._getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via1, via2);
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
* @param {number} percentage
* @param {{x: number, y: number}} [via1=this._getViaCoordinates()[0]]
* @param {{x: number, y: number}} [via2=this._getViaCoordinates()[1]]
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage, [via1, via2] = this._getViaCoordinates()) {
let t = percentage;
let vec = [];
vec[0] = Math.pow(1 - t, 3);
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
let x = vec[0] * this.fromPoint.x + vec[1] * via1.x + vec[2] * via2.x + vec[3] * this.toPoint.x;
let y = vec[0] * this.fromPoint.y + vec[1] * via1.y + vec[2] * via2.y + vec[3] * this.toPoint.y;
return {x: x, y: y};
}
}
export default CubicBezierEdge;

View File

@@ -0,0 +1,103 @@
import EdgeBase from './util/EdgeBase'
/**
* A Straight Edge.
*
* @extends EdgeBase
*/
class StraightEdge extends EdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Draw a line between two nodes
* @param {CanvasRenderingContext2D} ctx
* @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
* @private
*/
_line(ctx, values) {
// draw a straight line
ctx.beginPath();
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
ctx.lineTo(this.toPoint.x, this.toPoint.y);
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
this.disableShadow(ctx, values);
}
/**
*
* @returns {undefined}
*/
getViaNode() {
return undefined;
}
/**
* Combined function of pointOnLine and pointOnBezier. This gives the coordinates of a point on the line at a certain percentage of the way
*
* @param {number} percentage
* @returns {{x: number, y: number}}
* @private
*/
getPoint(percentage) {
return {
x: (1 - percentage) * this.fromPoint.x + percentage * this.toPoint.x,
y: (1 - percentage) * this.fromPoint.y + percentage * this.toPoint.y
}
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @returns {{x: number, y: number}}
* @private
*/
_findBorderPosition(nearNode, ctx) {
let node1 = this.to;
let node2 = this.from;
if (nearNode.id === this.from.id) {
node1 = this.from;
node2 = this.to;
}
let angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
let dx = (node1.x - node2.x);
let dy = (node1.y - node2.y);
let edgeSegmentLength = Math.sqrt(dx * dx + dy * dy);
let toBorderDist = nearNode.distanceToBorder(ctx, angle);
let toBorderPoint = (edgeSegmentLength - toBorderDist) / edgeSegmentLength;
let borderPos = {};
borderPos.x = (1 - toBorderPoint) * node2.x + toBorderPoint * node1.x;
borderPos.y = (1 - toBorderPoint) * node2.y + toBorderPoint * node1.y;
return borderPos;
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @returns {number}
* @private
*/
_getDistanceToEdge(x1, y1, x2, y2, x3, y3) { // x3,y3 is the point
return this._getDistanceToLine(x1, y1, x2, y2, x3, y3);
}
}
export default StraightEdge;

View File

@@ -0,0 +1,159 @@
import EdgeBase from './EdgeBase'
/**
* The Base Class for all Bezier edges. Bezier curves are used to model smooth
* gradual curves in paths between nodes.
*
* @extends EdgeBase
*/
class BezierEdgeBase extends EdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* This function uses binary search to look for the point where the bezier curve crosses the border of the node.
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @param {Node} viaNode
* @returns {*}
* @private
*/
_findBorderPositionBezier(nearNode, ctx, viaNode = this._getViaCoordinates()) {
var maxIterations = 10;
var iteration = 0;
var low = 0;
var high = 1;
var pos, angle, distanceToBorder, distanceToPoint, difference;
var threshold = 0.2;
var node = this.to;
var from = false;
if (nearNode.id === this.from.id) {
node = this.from;
from = true;
}
while (low <= high && iteration < maxIterations) {
var middle = (low + high) * 0.5;
pos = this.getPoint(middle, viaNode);
angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
distanceToBorder = node.distanceToBorder(ctx, angle);
distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
difference = distanceToBorder - distanceToPoint;
if (Math.abs(difference) < threshold) {
break; // found
}
else if (difference < 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
if (from === false) {
low = middle;
}
else {
high = middle;
}
}
else {
if (from === false) {
high = middle;
}
else {
low = middle;
}
}
iteration++;
}
pos.t = middle;
return pos;
}
/**
* Calculate the distance between a point (x3,y3) and a line segment from
* (x1,y1) to (x2,y2).
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
* @param {number} x1 from x
* @param {number} y1 from y
* @param {number} x2 to x
* @param {number} y2 to y
* @param {number} x3 point to check x
* @param {number} y3 point to check y
* @param {Node} via
* @returns {number}
* @private
*/
_getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via) { // x3,y3 is the point
let minDistance = 1e9;
let distance;
let i, t, x, y;
let lastX = x1;
let lastY = y1;
for (i = 1; i < 10; i++) {
t = 0.1 * i;
x = Math.pow(1 - t, 2) * x1 + (2 * t * (1 - t)) * via.x + Math.pow(t, 2) * x2;
y = Math.pow(1 - t, 2) * y1 + (2 * t * (1 - t)) * via.y + Math.pow(t, 2) * y2;
if (i > 0) {
distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
minDistance = distance < minDistance ? distance : minDistance;
}
lastX = x;
lastY = y;
}
return minDistance;
}
/**
* Draw a bezier curve between two nodes
*
* The method accepts zero, one or two control points.
* Passing zero control points just draws a straight line
*
* @param {CanvasRenderingContext2D} ctx
* @param {Object} values | options for shadow drawing
* @param {Object|undefined} viaNode1 | first control point for curve drawing
* @param {Object|undefined} viaNode2 | second control point for curve drawing
*
* @protected
*/
_bezierCurve(ctx, values, viaNode1, viaNode2) {
var hasNode1 = (viaNode1 !== undefined && viaNode1.x !== undefined);
var hasNode2 = (viaNode2 !== undefined && viaNode2.x !== undefined);
ctx.beginPath();
ctx.moveTo(this.fromPoint.x, this.fromPoint.y);
if (hasNode1 && hasNode2) {
ctx.bezierCurveTo(viaNode1.x, viaNode1.y, viaNode2.x, viaNode2.y, this.toPoint.x, this.toPoint.y);
} else if (hasNode1) {
ctx.quadraticCurveTo(viaNode1.x, viaNode1.y, this.toPoint.x, this.toPoint.y);
} else {
// fallback to normal straight edge
ctx.lineTo(this.toPoint.x, this.toPoint.y);
}
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
this.disableShadow(ctx, values);
}
/**
*
* @returns {*|{x, y}|{x: undefined, y: undefined}}
*/
getViaNode() {
return this._getViaCoordinates();
}
}
export default BezierEdgeBase;

View File

@@ -0,0 +1,62 @@
import BezierEdgeBase from './BezierEdgeBase'
/**
* A Base Class for all Cubic Bezier Edges. Bezier curves are used to model
* smooth gradual curves in paths between nodes.
*
* @extends BezierEdgeBase
*/
class CubicBezierEdgeBase extends BezierEdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
* Calculate the distance between a point (x3,y3) and a line segment from
* (x1,y1) to (x2,y2).
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
* https://en.wikipedia.org/wiki/B%C3%A9zier_curve
* @param {number} x1 from x
* @param {number} y1 from y
* @param {number} x2 to x
* @param {number} y2 to y
* @param {number} x3 point to check x
* @param {number} y3 point to check y
* @param {Node} via1
* @param {Node} via2
* @returns {number}
* @private
*/
_getDistanceToBezierEdge(x1, y1, x2, y2, x3, y3, via1, via2) { // x3,y3 is the point
let minDistance = 1e9;
let distance;
let i, t, x, y;
let lastX = x1;
let lastY = y1;
let vec = [0,0,0,0]
for (i = 1; i < 10; i++) {
t = 0.1 * i;
vec[0] = Math.pow(1 - t, 3);
vec[1] = 3 * t * Math.pow(1 - t, 2);
vec[2] = 3 * Math.pow(t,2) * (1 - t);
vec[3] = Math.pow(t, 3);
x = vec[0] * x1 + vec[1] * via1.x + vec[2] * via2.x + vec[3] * x2;
y = vec[0] * y1 + vec[1] * via1.y + vec[2] * via2.y + vec[3] * y2;
if (i > 0) {
distance = this._getDistanceToLine(lastX, lastY, x, y, x3, y3);
minDistance = distance < minDistance ? distance : minDistance;
}
lastX = x;
lastY = y;
}
return minDistance;
}
}
export default CubicBezierEdgeBase;

View File

@@ -0,0 +1,597 @@
let util = require("../../../../../util");
let EndPoints = require("./EndPoints").default;
/**
* The Base Class for all edges.
*
*/
class EdgeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
this.body = body;
this.labelModule = labelModule;
this.options = {};
this.setOptions(options);
this.colorDirty = true;
this.color = {};
this.selectionWidth = 2;
this.hoverWidth = 1.5;
this.fromPoint = this.from;
this.toPoint = this.to;
}
/**
* Connects a node to itself
*/
connect() {
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
}
/**
*
* @returns {boolean} always false
*/
cleanup() {
return false;
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
this.from = this.body.nodes[this.options.from];
this.to = this.body.nodes[this.options.to];
this.id = this.options.id;
}
/**
* Redraw a edge as a line
* Draw this edge in the given canvas
* The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d");
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {boolean} selected
* @param {boolean} hover
* @param {Node} viaNode
* @private
*/
drawLine(ctx, values, selected, hover, viaNode) {
// set style
ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
ctx.lineWidth = values.width;
if (values.dashes !== false) {
this._drawDashedLine(ctx, values, viaNode);
}
else {
this._drawLine(ctx, values, viaNode);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {Node} viaNode
* @param {{x: number, y: number}} [fromPoint]
* @param {{x: number, y: number}} [toPoint]
* @private
*/
_drawLine(ctx, values, viaNode, fromPoint, toPoint) {
if (this.from != this.to) {
// draw line
this._line(ctx, values, viaNode, fromPoint, toPoint);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {Node} viaNode
* @param {{x: number, y: number}} [fromPoint] TODO: Remove in next major release
* @param {{x: number, y: number}} [toPoint] TODO: Remove in next major release
* @private
*/
_drawDashedLine(ctx, values, viaNode, fromPoint, toPoint) { // eslint-disable-line no-unused-vars
ctx.lineCap = 'round';
let pattern = [5,5];
if (Array.isArray(values.dashes) === true) {
pattern = values.dashes;
}
// only firefox and chrome support this method, else we use the legacy one.
if (ctx.setLineDash !== undefined) {
ctx.save();
// set dash settings for chrome or firefox
ctx.setLineDash(pattern);
ctx.lineDashOffset = 0;
// draw the line
if (this.from != this.to) {
// draw line
this._line(ctx, values, viaNode);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
// restore the dash settings.
ctx.setLineDash([0]);
ctx.lineDashOffset = 0;
ctx.restore();
}
else { // unsupporting smooth lines
if (this.from != this.to) {
// draw line
ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern);
}
else {
let [x,y,radius] = this._getCircleData(ctx);
this._circle(ctx, values, x, y, radius);
}
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.stroke();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
}
/**
*
* @param {Node} nearNode
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {{x: number, y: number}}
*/
findBorderPosition(nearNode, ctx, options) {
if (this.from != this.to) {
return this._findBorderPosition(nearNode, ctx, options);
}
else {
return this._findBorderPositionCircle(nearNode, ctx, options);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @returns {{from: ({x: number, y: number, t: number}|*), to: ({x: number, y: number, t: number}|*)}}
*/
findBorderPositions(ctx) {
let from = {};
let to = {};
if (this.from != this.to) {
from = this._findBorderPosition(this.from, ctx);
to = this._findBorderPosition(this.to, ctx);
}
else {
let [x,y] = this._getCircleData(ctx).slice(0, 2);
from = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.25, high:0.6, direction:-1});
to = this._findBorderPositionCircle(this.from, ctx, {x, y, low:0.6, high:0.8, direction:1});
}
return {from, to};
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @returns {Array.<number>} x, y, radius
* @private
*/
_getCircleData(ctx) {
let x, y;
let node = this.from;
let radius = this.options.selfReferenceSize;
if (ctx !== undefined) {
if (node.shape.width === undefined) {
node.shape.resize(ctx);
}
}
// get circle coordinates
if (node.shape.width > node.shape.height) {
x = node.x + node.shape.width * 0.5;
y = node.y - radius;
}
else {
x = node.x + radius;
y = node.y - node.shape.height * 0.5;
}
return [x,y,radius];
}
/**
* 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) {
let angle = percentage * 2 * Math.PI;
return {
x: x + radius * Math.cos(angle),
y: y - radius * Math.sin(angle)
}
}
/**
* This function uses binary search to look for the point where the circle crosses the border of the node.
* @param {Node} node
* @param {CanvasRenderingContext2D} ctx
* @param {Object} options
* @returns {*}
* @private
*/
_findBorderPositionCircle(node, ctx, options) {
let x = options.x;
let y = options.y;
let low = options.low;
let high = options.high;
let direction = options.direction;
let maxIterations = 10;
let iteration = 0;
let radius = this.options.selfReferenceSize;
let pos, angle, distanceToBorder, distanceToPoint, difference;
let threshold = 0.05;
let middle = (low + high) * 0.5;
while (low <= high && iteration < maxIterations) {
middle = (low + high) * 0.5;
pos = this._pointOnCircle(x, y, radius, middle);
angle = Math.atan2((node.y - pos.y), (node.x - pos.x));
distanceToBorder = node.distanceToBorder(ctx, angle);
distanceToPoint = Math.sqrt(Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2));
difference = distanceToBorder - distanceToPoint;
if (Math.abs(difference) < threshold) {
break; // found
}
else if (difference > 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node.
if (direction > 0) {
low = middle;
}
else {
high = middle;
}
}
else {
if (direction > 0) {
high = middle;
}
else {
low = middle;
}
}
iteration++;
}
pos.t = middle;
return pos;
}
/**
* Get the line width of the edge. Depends on width and whether one of the
* connected nodes is selected.
* @param {boolean} selected
* @param {boolean} hover
* @returns {number} width
* @private
*/
getLineWidth(selected, hover) {
if (selected === true) {
return Math.max(this.selectionWidth, 0.3 / this.body.view.scale);
}
else {
if (hover === true) {
return Math.max(this.hoverWidth, 0.3 / this.body.view.scale);
}
else {
return Math.max(this.options.width, 0.3 / this.body.view.scale);
}
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
* @param {boolean} selected - Unused
* @param {boolean} hover - Unused
* @returns {string}
*/
getColor(ctx, values, selected, hover) { // eslint-disable-line no-unused-vars
if (values.inheritsColor !== false) {
// when this is a loop edge, just use the 'from' method
if ((values.inheritsColor === 'both') && (this.from.id !== this.to.id)) {
let grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
let fromColor, toColor;
fromColor = this.from.options.color.highlight.border;
toColor = this.to.options.color.highlight.border;
if ((this.from.selected === false) && (this.to.selected === false)) {
fromColor = util.overrideOpacity(this.from.options.color.border, values.opacity);
toColor = util.overrideOpacity(this.to.options.color.border, values.opacity);
}
else if ((this.from.selected === true) && (this.to.selected === false)) {
toColor = this.to.options.color.border;
}
else if ((this.from.selected === false) && (this.to.selected === true)) {
fromColor = this.from.options.color.border;
}
grd.addColorStop(0, fromColor);
grd.addColorStop(1, toColor);
// -------------------- this returns -------------------- //
return grd;
}
if (values.inheritsColor === "to") {
return util.overrideOpacity(this.to.options.color.border, values.opacity);
} else { // "from"
return util.overrideOpacity(this.from.options.color.border, values.opacity);
}
} else {
return util.overrideOpacity(values.color, values.opacity);
}
}
/**
* Draw a line from a node to itself, a circle
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array} values
* @param {number} x
* @param {number} y
* @param {number} radius
* @private
*/
_circle(ctx, values, x, y, radius) {
// draw shadow if enabled
this.enableShadow(ctx, values);
// draw a circle
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.stroke();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
/**
* Calculate the distance between a point (x3,y3) and a line segment from (x1,y1) to (x2,y2).
* (x3,y3) is the point.
*
* http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @param {Node} via
* @param {Array} values
* @returns {number}
*/
getDistanceToEdge(x1, y1, x2, y2, x3, y3, via, values) { // eslint-disable-line no-unused-vars
let returnValue = 0;
if (this.from != this.to) {
returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via)
}
else {
let [x,y,radius] = this._getCircleData(undefined);
let dx = x - x3;
let dy = y - y3;
returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius);
}
return returnValue;
}
/**
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {number} x3
* @param {number} y3
* @returns {number}
* @private
*/
_getDistanceToLine(x1, y1, x2, y2, x3, y3) {
let px = x2 - x1;
let py = y2 - y1;
let something = px * px + py * py;
let u = ((x3 - x1) * px + (y3 - y1) * py) / something;
if (u > 1) {
u = 1;
}
else if (u < 0) {
u = 0;
}
let x = x1 + u * px;
let y = y1 + u * py;
let dx = x - x3;
let dy = y - y3;
//# Note: If the actual distance does not matter,
//# if you only want to compare what this function
//# returns to other results of this function, you
//# can just return the squared distance instead
//# (i.e. remove the sqrt) to gain a little performance
return Math.sqrt(dx * dx + dy * dy);
}
/**
* @param {CanvasRenderingContext2D} ctx
* @param {string} position
* @param {Node} viaNode
* @param {boolean} selected
* @param {boolean} hover
* @param {Array} values
* @returns {{point: *, core: {x: number, y: number}, angle: *, length: number, type: *}}
*/
getArrowData(ctx, position, viaNode, selected, hover, values) {
// set lets
let angle;
let arrowPoint;
let node1;
let node2;
let guideOffset;
let scaleFactor;
let type;
let lineWidth = values.width;
if (position === 'from') {
node1 = this.from;
node2 = this.to;
guideOffset = 0.1;
scaleFactor = values.fromArrowScale;
type = values.fromArrowType;
}
else if (position === 'to') {
node1 = this.to;
node2 = this.from;
guideOffset = -0.1;
scaleFactor = values.toArrowScale;
type = values.toArrowType;
}
else {
node1 = this.to;
node2 = this.from;
scaleFactor = values.middleArrowScale;
type = values.middleArrowType;
}
// if not connected to itself
if (node1 != node2) {
if (position !== 'middle') {
// draw arrow head
if (this.options.smooth.enabled === true) {
arrowPoint = this.findBorderPosition(node1, ctx, { via: viaNode });
let guidePos = this.getPoint(Math.max(0.0, Math.min(1.0, arrowPoint.t + guideOffset)), viaNode);
angle = Math.atan2((arrowPoint.y - guidePos.y), (arrowPoint.x - guidePos.x));
} else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPoint = this.findBorderPosition(node1, ctx);
}
} else {
angle = Math.atan2((node1.y - node2.y), (node1.x - node2.x));
arrowPoint = this.getPoint(0.5, viaNode); // this is 0.6 to account for the size of the arrow.
}
} else {
// draw circle
let [x,y,radius] = this._getCircleData(ctx);
if (position === 'from') {
arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.25, high: 0.6, direction: -1 });
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
} else if (position === 'to') {
arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.6, high: 1.0, direction: 1 });
angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI;
} else {
arrowPoint = this._pointOnCircle(x, y, radius, 0.175);
angle = 3.9269908169872414; // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI;
}
}
if (position === 'middle' && scaleFactor < 0) lineWidth *= -1; // reversed middle arrow
let length = 15 * scaleFactor + 3 * lineWidth; // 3* lineWidth is the width of the edge.
var xi = arrowPoint.x - length * 0.9 * Math.cos(angle);
var yi = arrowPoint.y - length * 0.9 * Math.sin(angle);
let arrowCore = { x: xi, y: yi };
return { point: arrowPoint, core: arrowCore, angle: angle, length: length, type: type };
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
* @param {boolean} selected
* @param {boolean} hover
* @param {Object} arrowData
*/
drawArrowHead(ctx, values, selected, hover, arrowData) {
// set style
ctx.strokeStyle = this.getColor(ctx, values, selected, hover);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = values.width;
EndPoints.draw(ctx, arrowData);
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
enableShadow(ctx, values) {
if (values.shadow === true) {
ctx.shadowColor = values.shadowColor;
ctx.shadowBlur = values.shadowSize;
ctx.shadowOffsetX = values.shadowX;
ctx.shadowOffsetY = values.shadowY;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
disableShadow(ctx, values) {
if (values.shadow === true) {
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
}
}
export default EdgeBase;

View File

@@ -0,0 +1,231 @@
/** ============================================================================
* Location of all the endpoint drawing routines.
*
* Every endpoint has its own drawing routine, which contains an endpoint definition.
*
* The endpoint definitions must have the following properies:
*
* - (0,0) is the connection point to the node it attaches to
* - The endpoints are orientated to the positive x-direction
* - The length of the endpoint is at most 1
*
* As long as the endpoint classes remain simple and not too numerous, they will
* be contained within this module.
* All classes here except `EndPoints` should be considered as private to this module.
*
* -----------------------------------------------------------------------------
* ### Further Actions
*
* After adding a new endpoint here, you also need to do the following things:
*
* - Add the new endpoint name to `network/options.js` in array `endPoints`.
* - Add the new endpoint name to the documentation.
* Scan for 'arrows.to.type` and add it to the description.
* - Add the endpoint to the examples. At the very least, add it to example
* `edgeStyles/arrowTypes`.
* ============================================================================= */
// NOTE: When a typedef is isolated in a separate comment block, an actual description is generated for it,
// using the rest of the commenting in the code block. Usage of typedef in other comments then
// link to there. TIL.
//
// Also noteworthy, all typedef's set up in this manner are collected in a single, global page 'global.html'.
// In other words, it doesn't matter *where* the typedef's are defined in the code.
//
//
// TODO: add descriptive commenting to given typedef's
/**
* @typedef {{type:string, point:Point, angle:number, length:number}} ArrowData
*
* Object containing instantiation data for a given endpoint.
*/
/**
* @typedef {{x:number, y:number}} Point
*
* A point in view-coordinates.
*/
/**
* Common methods for endpoints
*
* @class
*/
class EndPoint {
/**
* Apply transformation on points for display.
*
* The following is done:
* - rotate by the specified angle
* - multiply the (normalized) coordinates by the passed length
* - offset by the target coordinates
*
* @param {Array<Point>} points
* @param {ArrowData} arrowData
* @static
*/
static transform(points, arrowData) {
if (!(points instanceof Array)) {
points = [points];
}
var x = arrowData.point.x;
var y = arrowData.point.y;
var angle = arrowData.angle
var length = arrowData.length;
for(var i = 0; i < points.length; ++i) {
var p = points[i];
var xt = p.x * Math.cos(angle) - p.y * Math.sin(angle);
var yt = p.x * Math.sin(angle) + p.y * Math.cos(angle);
p.x = x + length*xt;
p.y = y + length*yt;
}
}
/**
* Draw a closed path using the given real coordinates.
*
* @param {CanvasRenderingContext2D} ctx
* @param {Array.<Point>} points
* @static
*/
static drawPath(ctx, points) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for(var i = 1; i < points.length; ++i) {
ctx.lineTo(points[i].x, points[i].y);
}
ctx.closePath();
}
}
/**
* Drawing methods for the arrow endpoint.
* @extends EndPoint
*/
class Arrow extends EndPoint {
/**
* Draw this shape at the end of a line.
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowData} arrowData
* @static
*/
static draw(ctx, arrowData) {
// Normalized points of closed path, in the order that they should be drawn.
// (0, 0) is the attachment point, and the point around which should be rotated
var points = [
{ x: 0 , y: 0 },
{ x:-1 , y: 0.3},
{ x:-0.9, y: 0 },
{ x:-1 , y:-0.3},
];
EndPoint.transform(points, arrowData);
EndPoint.drawPath(ctx, points);
}
}
/**
* Drawing methods for the circle endpoint.
*/
class Circle {
/**
* Draw this shape at the end of a line.
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowData} arrowData
* @static
*/
static draw(ctx, arrowData) {
var point = {x:-0.4, y:0};
EndPoint.transform(point, arrowData);
ctx.circle(point.x, point.y, arrowData.length*0.4);
}
}
/**
* Drawing methods for the bar endpoint.
*/
class Bar {
/**
* Draw this shape at the end of a line.
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowData} arrowData
* @static
*/
static draw(ctx, arrowData) {
/*
var points = [
{x:0, y:0.5},
{x:0, y:-0.5}
];
EndPoint.transform(points, arrowData);
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.stroke();
*/
var points = [
{x:0, y:0.5},
{x:0, y:-0.5},
{x:-0.15, y:-0.5},
{x:-0.15, y:0.5},
];
EndPoint.transform(points, arrowData);
EndPoint.drawPath(ctx, points);
}
}
/**
* Drawing methods for the endpoints.
*/
class EndPoints {
/**
* Draw an endpoint
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowData} arrowData
* @static
*/
static draw(ctx, arrowData) {
var type;
if (arrowData.type) {
type = arrowData.type.toLowerCase();
}
switch (type) {
case 'circle':
Circle.draw(ctx, arrowData);
break;
case 'bar':
Bar.draw(ctx, arrowData);
break;
case 'arrow': // fall-through
default:
Arrow.draw(ctx, arrowData);
}
}
}
export default EndPoints;

View File

@@ -0,0 +1,85 @@
let util = require("../../../../util");
let Node = require("../Node").default;
/**
* A Cluster is a special Node that allows a group of Nodes positioned closely together
* to be represented by a single Cluster Node.
*
* @extends Node
*/
class Cluster extends Node {
/**
* @param {Object} options
* @param {Object} body
* @param {Array.<HTMLImageElement>}imagelist
* @param {Array} grouplist
* @param {Object} globalOptions
* @param {Object} defaultOptions Global default options for nodes
*/
constructor(options, body, imagelist, grouplist, globalOptions, defaultOptions) {
super(options, body, imagelist, grouplist, globalOptions, defaultOptions);
this.isCluster = true;
this.containedNodes = {};
this.containedEdges = {};
}
/**
* Transfer child cluster data to current and disconnect the child cluster.
*
* Please consult the header comment in 'Clustering.js' for the fields set here.
*
* @param {string|number} childClusterId id of child cluster to open
*/
_openChildCluster(childClusterId) {
let childCluster = this.body.nodes[childClusterId];
if (this.containedNodes[childClusterId] === undefined) {
throw new Error('node with id: ' + childClusterId + ' not in current cluster');
}
if (!childCluster.isCluster) {
throw new Error('node with id: ' + childClusterId + ' is not a cluster');
}
// Disconnect child cluster from current cluster
delete this.containedNodes[childClusterId];
util.forEach(childCluster.edges, (edge) => {
delete this.containedEdges[edge.id];
});
// Transfer nodes and edges
util.forEach(childCluster.containedNodes, (node, nodeId) => {
this.containedNodes[nodeId] = node;
});
childCluster.containedNodes = {};
util.forEach(childCluster.containedEdges, (edge, edgeId) => {
this.containedEdges[edgeId] = edge;
});
childCluster.containedEdges = {};
// Transfer edges within cluster edges which are clustered
util.forEach(childCluster.edges, (clusterEdge) => {
util.forEach(this.edges, (parentClusterEdge) => {
// Assumption: a clustered edge can only be present in a single clustering edge
// Not tested here
let index = parentClusterEdge.clusteringEdgeReplacingIds.indexOf(clusterEdge.id);
if (index === -1) return;
util.forEach(clusterEdge.clusteringEdgeReplacingIds, (srcId) => {
parentClusterEdge.clusteringEdgeReplacingIds.push(srcId);
// Maintain correct bookkeeping for transferred edge
this.body.edges[srcId].edgeReplacedById = parentClusterEdge.id;
});
// Remove cluster edge from parent cluster edge
parentClusterEdge.clusteringEdgeReplacingIds.splice(index, 1);
});
});
childCluster.edges = [];
}
}
export default Cluster;

View File

@@ -0,0 +1,91 @@
'use strict';
import NodeBase from '../util/NodeBase'
/**
* A Box Node/Cluster shape.
*
* @extends NodeBase
*/
class Box extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor (options, body, labelModule) {
super(options,body,labelModule);
this._setMargins(labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected = this.selected, hover = this.hover) {
if (this.needsRefresh(selected, hover)) {
var dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
this.width = dimensions.width + this.margin.right + this.margin.left;
this.height = dimensions.height + this.margin.top + this.margin.bottom;
this.radius = this.width / 2;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.left = x - this.width / 2;
this.top = y - this.height / 2;
this.initContextForDraw(ctx, values);
ctx.roundRect(this.left, this.top, this.width, this.height, values.borderRadius);
this.performFill(ctx, values);
this.updateBoundingBox(x, y, ctx, selected, hover);
this.labelModule.draw(ctx, this.left + this.textSize.width / 2 + this.margin.left,
this.top + this.textSize.height / 2 + this.margin.top, selected, hover);
}
/**
*
* @param {number} x width
* @param {number} y height
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
*/
updateBoundingBox(x, y, ctx, selected, hover) {
this._updateBoundingBox(x, y, ctx, selected, hover);
let borderRadius = this.options.shapeProperties.borderRadius; // only effective for box
this._addBoundingBoxMargin(borderRadius);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
this.resize(ctx);
let borderWidth = this.options.borderWidth;
return Math.min(
Math.abs((this.width) / 2 / Math.cos(angle)),
Math.abs((this.height) / 2 / Math.sin(angle))) + borderWidth;
}
}
export default Box;

View File

@@ -0,0 +1,86 @@
'use strict';
import CircleImageBase from '../util/CircleImageBase'
/**
* A Circle Node/Cluster shape.
*
* @extends CircleImageBase
*/
class Circle extends CircleImageBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
this._setMargins(labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected = this.selected, hover = this.hover) {
if (this.needsRefresh(selected, hover)) {
var dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
var diameter = Math.max(dimensions.width + this.margin.right + this.margin.left,
dimensions.height + this.margin.top + this.margin.bottom);
this.options.size = diameter / 2; // NOTE: this size field only set here, not in Ellipse, Database, Box
this.width = diameter;
this.height = diameter;
this.radius = this.width / 2;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.left = x - this.width / 2;
this.top = y - this.height / 2;
this._drawRawCircle(ctx, x, y, values);
this.updateBoundingBox(x,y);
this.labelModule.draw(ctx, this.left + this.textSize.width / 2 + this.margin.left,
y, selected, hover);
}
/**
*
* @param {number} x width
* @param {number} y height
*/
updateBoundingBox(x, y) {
this.boundingBox.top = y - this.options.size;
this.boundingBox.left = x - this.options.size;
this.boundingBox.right = x + this.options.size;
this.boundingBox.bottom = y + this.options.size;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle - Unused
* @returns {number}
*/
distanceToBorder(ctx, angle) { // eslint-disable-line no-unused-vars
this.resize(ctx);
return this.width * 0.5;
}
}
export default Circle;

View File

@@ -0,0 +1,112 @@
'use strict';
import CircleImageBase from '../util/CircleImageBase'
/**
* A CircularImage Node/Cluster shape.
*
* @extends CircleImageBase
*/
class CircularImage extends CircleImageBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
* @param {Image} imageObj
* @param {Image} imageObjAlt
*/
constructor (options, body, labelModule, imageObj, imageObjAlt) {
super(options, body, labelModule);
this.setImages(imageObj, imageObjAlt);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected = this.selected, hover = this.hover) {
var imageAbsent = (this.imageObj.src === undefined) ||
(this.imageObj.width === undefined) ||
(this.imageObj.height === undefined);
if (imageAbsent) {
var diameter = this.options.size * 2;
this.width = diameter;
this.height = diameter;
this.radius = 0.5*this.width;
return;
}
// At this point, an image is present, i.e. this.imageObj is valid.
if (this.needsRefresh(selected, hover)) {
this._resizeImage();
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.switchImages(selected);
this.resize();
this.left = x - this.width / 2;
this.top = y - this.height / 2;
// draw the background circle. IMPORTANT: the stroke in this method is used by the clip method below.
this._drawRawCircle(ctx, x, y, values);
// now we draw in the circle, we save so we can revert the clip operation after drawing.
ctx.save();
// clip is used to use the stroke in drawRawCircle as an area that we can draw in.
ctx.clip();
// draw the image
this._drawImageAtPosition(ctx, values);
// restore so we can again draw on the full canvas
ctx.restore();
this._drawImageLabel(ctx, x, y, selected, hover);
this.updateBoundingBox(x,y);
}
// TODO: compare with Circle.updateBoundingBox(), consolidate? More stuff is happening here
/**
*
* @param {number} x width
* @param {number} y height
*/
updateBoundingBox(x,y) {
this.boundingBox.top = y - this.options.size;
this.boundingBox.left = x - this.options.size;
this.boundingBox.right = x + this.options.size;
this.boundingBox.bottom = y + this.options.size;
// TODO: compare with Image.updateBoundingBox(), consolidate?
this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left);
this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width);
this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle - Unused
* @returns {number}
*/
distanceToBorder(ctx, angle) { // eslint-disable-line no-unused-vars
this.resize(ctx);
return this.width * 0.5;
}
}
export default CircularImage;

View File

@@ -0,0 +1,71 @@
'use strict';
import NodeBase from '../util/NodeBase'
/**
* A Database Node/Cluster shape.
*
* @extends NodeBase
*/
class Database extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor (options, body, labelModule) {
super(options, body, labelModule);
this._setMargins(labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
*/
resize(ctx, selected, hover) {
if (this.needsRefresh(selected, hover)) {
var dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
var size = dimensions.width + this.margin.right + this.margin.left;
this.width = size;
this.height = size;
this.radius = this.width / 2;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.left = x - this.width / 2;
this.top = y - this.height / 2;
this.initContextForDraw(ctx, values);
ctx.database(x - this.width / 2, y - this.height / 2, this.width, this.height);
this.performFill(ctx, values);
this.updateBoundingBox(x, y, ctx, selected, hover);
this.labelModule.draw(ctx, this.left + this.textSize.width / 2 + this.margin.left,
this.top + this.textSize.height / 2 + this.margin.top, selected, hover);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx, angle);
}
}
export default Database;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Diamond Node/Cluster shape.
*
* @extends ShapeBase
*/
class Diamond extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'diamond', 4, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Diamond;

View File

@@ -0,0 +1,45 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Dot Node/Cluster shape.
*
* @extends ShapeBase
*/
class Dot extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'circle', 2, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) { // eslint-disable-line no-unused-vars
this.resize(ctx);
return this.options.size;
}
}
export default Dot;

View File

@@ -0,0 +1,74 @@
'use strict';
import NodeBase from '../util/NodeBase'
/**
* Am Ellipse Node/Cluster shape.
*
* @extends NodeBase
*/
class Ellipse extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected = this.selected, hover = this.hover) {
if (this.needsRefresh(selected, hover)) {
var dimensions = this.getDimensionsFromLabel(ctx, selected, hover);
this.height = dimensions.height * 2;
this.width = dimensions.width + dimensions.height;
this.radius = 0.5*this.width;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.left = x - this.width * 0.5;
this.top = y - this.height * 0.5;
this.initContextForDraw(ctx, values);
ctx.ellipse_vis(this.left, this.top, this.width, this.height);
this.performFill(ctx, values);
this.updateBoundingBox(x, y, ctx, selected, hover);
this.labelModule.draw(ctx, x, y, selected, hover);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
this.resize(ctx);
var a = this.width * 0.5;
var b = this.height * 0.5;
var w = (Math.sin(angle) * a);
var h = (Math.cos(angle) * b);
return a * b / Math.sqrt(w * w + h * h);
}
}
export default Ellipse;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Hexagon Node/Cluster shape.
*
* @extends ShapeBase
*/
class Hexagon extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'hexagon', 4, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Hexagon;

View File

@@ -0,0 +1,127 @@
'use strict';
import NodeBase from '../util/NodeBase'
/**
* An icon replacement for the default Node shape.
*
* @extends NodeBase
*/
class Icon extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
this._setMargins(labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx - Unused.
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected, hover) {
if (this.needsRefresh(selected, hover)) {
this.iconSize = {
width: Number(this.options.icon.size),
height: Number(this.options.icon.size)
};
this.width = this.iconSize.width + this.margin.right + this.margin.left;
this.height = this.iconSize.height + this.margin.top + this.margin.bottom;
this.radius = 0.5*this.width;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.options.icon.size = this.options.icon.size || 50;
this.left = x - this.width / 2;
this.top = y - this.height / 2;
this._icon(ctx, x, y, selected, hover, values);
if (this.options.label !== undefined) {
var iconTextSpacing = 5;
this.labelModule.draw(ctx, this.left + this.iconSize.width / 2 + this.margin.left,
y + this.height / 2 + iconTextSpacing, selected);
}
this.updateBoundingBox(x, y)
}
/**
*
* @param {number} x
* @param {number} y
*/
updateBoundingBox(x, y) {
this.boundingBox.top = y - this.options.icon.size * 0.5;
this.boundingBox.left = x - this.options.icon.size * 0.5;
this.boundingBox.right = x + this.options.icon.size * 0.5;
this.boundingBox.bottom = y + this.options.icon.size * 0.5;
if (this.options.label !== undefined && this.labelModule.size.width > 0) {
var iconTextSpacing = 5;
this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left);
this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width);
this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height + iconTextSpacing);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover - Unused
* @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
*/
_icon(ctx, x, y, selected, hover, values) {
let iconSize = Number(this.options.icon.size);
if (this.options.icon.code !== undefined) {
ctx.font = (selected ? "bold " : "") + iconSize + "px " + this.options.icon.face;
// draw icon
ctx.fillStyle = this.options.icon.color || "black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// draw shadow if enabled
this.enableShadow(ctx, values);
ctx.fillText(this.options.icon.code, x, y);
// disable shadows for other elements.
this.disableShadow(ctx, values);
} else {
console.error('When using the icon shape, you need to define the code in the icon options object. This can be done per node or globally.')
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Icon;

View File

@@ -0,0 +1,123 @@
'use strict';
import CircleImageBase from '../util/CircleImageBase'
/**
* An image-based replacement for the default Node shape.
*
* @extends CircleImageBase
*/
class Image extends CircleImageBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
* @param {Image} imageObj
* @param {Image} imageObjAlt
*/
constructor (options, body, labelModule, imageObj, imageObjAlt) {
super(options, body, labelModule);
this.setImages(imageObj, imageObjAlt);
}
/**
*
* @param {CanvasRenderingContext2D} ctx - Unused.
* @param {boolean} [selected]
* @param {boolean} [hover]
*/
resize(ctx, selected = this.selected, hover = this.hover) {
var imageAbsent = (this.imageObj.src === undefined) ||
(this.imageObj.width === undefined) ||
(this.imageObj.height === undefined);
if (imageAbsent) {
var side = this.options.size * 2;
this.width = side;
this.height = side;
return;
}
if (this.needsRefresh(selected, hover)) {
this._resizeImage();
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.switchImages(selected);
this.resize();
this.left = x - this.width / 2;
this.top = y - this.height / 2;
if (this.options.shapeProperties.useBorderWithImage === true) {
var neutralborderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var borderWidth = (selected ? selectionLineWidth : neutralborderWidth) / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.beginPath();
// setup the line properties.
ctx.strokeStyle = selected ? this.options.color.highlight.border : hover ? this.options.color.hover.border : this.options.color.border;
// set a fillstyle
ctx.fillStyle = selected ? this.options.color.highlight.background : hover ? this.options.color.hover.background : this.options.color.background;
// draw a rectangle to form the border around. This rectangle is filled so the opacity of a picture (in future vis releases?) can be used to tint the image
ctx.rect(this.left - 0.5 * ctx.lineWidth,
this.top - 0.5 * ctx.lineWidth,
this.width + ctx.lineWidth,
this.height + ctx.lineWidth);
ctx.fill();
this.performStroke(ctx, values);
ctx.closePath();
}
this._drawImageAtPosition(ctx, values);
this._drawImageLabel(ctx, x, y, selected, hover);
this.updateBoundingBox(x,y);
}
/**
*
* @param {number} x
* @param {number} y
*/
updateBoundingBox(x, y) {
this.resize();
this._updateBoundingBox(x, y);
if (this.options.label !== undefined && this.labelModule.size.width > 0) {
this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left);
this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width);
this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelOffset);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Image;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Square Node/Cluster shape.
*
* @extends ShapeBase
*/
class Square extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'square', 2, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Square;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Star Node/Cluster shape.
*
* @extends ShapeBase
*/
class Star extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'star', 4, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Star;

View File

@@ -0,0 +1,72 @@
'use strict';
import NodeBase from '../util/NodeBase'
/**
* A text-based replacement for the default Node shape.
*
* @extends NodeBase
*/
class Text extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
this._setMargins(labelModule);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
*/
resize(ctx, selected, hover) {
if (this.needsRefresh(selected, hover)) {
this.textSize = this.labelModule.getTextSize(ctx, selected, hover);
this.width = this.textSize.width + this.margin.right + this.margin.left;
this.height = this.textSize.height + this.margin.top + this.margin.bottom;
this.radius = 0.5*this.width;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this.resize(ctx, selected, hover);
this.left = x - this.width / 2;
this.top = y - this.height / 2;
// draw shadow if enabled
this.enableShadow(ctx, values);
this.labelModule.draw(ctx, this.left + this.textSize.width / 2 + this.margin.left,
this.top + this.textSize.height / 2 + this.margin.top, selected, hover);
// disable shadows for other elements.
this.disableShadow(ctx, values);
this.updateBoundingBox(x, y, ctx, selected, hover);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Text;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A Triangle Node/Cluster shape.
*
* @extends ShapeBase
*/
class Triangle extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'triangle', 3, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default Triangle;

View File

@@ -0,0 +1,44 @@
'use strict';
import ShapeBase from '../util/ShapeBase'
/**
* A downward facing Triangle Node/Cluster shape.
*
* @extends ShapeBase
*/
class TriangleDown extends ShapeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {boolean} selected
* @param {boolean} hover
* @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
*/
draw(ctx, x, y, selected, hover, values) {
this._drawShape(ctx, 'triangleDown', 3, x, y, selected, hover, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
*/
distanceToBorder(ctx, angle) {
return this._distanceToBorder(ctx,angle);
}
}
export default TriangleDown;

View File

@@ -0,0 +1,192 @@
import NodeBase from './NodeBase';
/**
* NOTE: This is a bad base class
*
* Child classes are:
*
* Image - uses *only* image methods
* Circle - uses *only* _drawRawCircle
* CircleImage - uses all
*
* TODO: Refactor, move _drawRawCircle to different module, derive Circle from NodeBase
* Rename this to ImageBase
* Consolidate common code in Image and CircleImage to base class
*
* @extends NodeBase
*/
class CircleImageBase extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule);
this.labelOffset = 0;
this.selected = false;
}
/**
*
* @param {Object} options
* @param {Object} [imageObj]
* @param {Object} [imageObjAlt]
*/
setOptions(options, imageObj, imageObjAlt) {
this.options = options;
if (!(imageObj === undefined && imageObjAlt === undefined)) {
this.setImages(imageObj, imageObjAlt);
}
}
/**
* Set the images for this node.
*
* The images can be updated after the initial setting of options;
* therefore, this method needs to be reentrant.
*
* For correct working in error cases, it is necessary to properly set
* field 'nodes.brokenImage' in the options.
*
* @param {Image} imageObj required; main image to show for this node
* @param {Image|undefined} imageObjAlt optional; image to show when node is selected
*/
setImages(imageObj, imageObjAlt) {
if (imageObjAlt && this.selected) {
this.imageObj = imageObjAlt;
this.imageObjAlt = imageObj;
} else {
this.imageObj = imageObj;
this.imageObjAlt = imageObjAlt;
}
}
/**
* Set selection and switch between the base and the selected image.
*
* Do the switch only if imageObjAlt exists.
*
* @param {boolean} selected value of new selected state for current node
*/
switchImages(selected) {
var selection_changed = ((selected && !this.selected) || (!selected && this.selected));
this.selected = selected; // Remember new selection
if (this.imageObjAlt !== undefined && selection_changed) {
let imageTmp = this.imageObj;
this.imageObj = this.imageObjAlt;
this.imageObjAlt = imageTmp;
}
}
/**
* Adjust the node dimensions for a loaded image.
*
* Pre: this.imageObj is valid
*/
_resizeImage() {
var width, height;
if (this.options.shapeProperties.useImageSize === false) {
// Use the size property
var ratio_width = 1;
var ratio_height = 1;
// Only calculate the proper ratio if both width and height not zero
if (this.imageObj.width && this.imageObj.height) {
if (this.imageObj.width > this.imageObj.height) {
ratio_width = this.imageObj.width / this.imageObj.height;
}
else {
ratio_height = this.imageObj.height / this.imageObj.width;
}
}
width = this.options.size * 2 * ratio_width;
height = this.options.size * 2 * ratio_height;
}
else {
// Use the image size
width = this.imageObj.width;
height = this.imageObj.height;
}
this.width = width;
this.height = height;
this.radius = 0.5 * this.width;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @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
* @private
*/
_drawRawCircle(ctx, x, y, values) {
this.initContextForDraw(ctx, values);
ctx.circle(x, y, values.size);
this.performFill(ctx, values);
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
* @private
*/
_drawImageAtPosition(ctx, values) {
if (this.imageObj.width != 0) {
// draw the image
ctx.globalAlpha = 1.0;
// draw shadow if enabled
this.enableShadow(ctx, values);
let factor = 1;
if (this.options.shapeProperties.interpolation === true) {
factor = (this.imageObj.width / this.width) / this.body.view.scale;
}
this.imageObj.drawImageAtPosition(ctx, factor, this.left, this.top, this.width, this.height);
// disable shadows for other elements.
this.disableShadow(ctx, values);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x width
* @param {number} y height
* @param {boolean} selected
* @param {boolean} hover
* @private
*/
_drawImageLabel(ctx, x, y, selected, hover) {
var yLabel;
var offset = 0;
if (this.height !== undefined) {
offset = this.height * 0.5;
var labelDimensions = this.labelModule.getTextSize(ctx, selected, hover);
if (labelDimensions.lineCount >= 1) {
offset += labelDimensions.height / 2;
}
}
yLabel = y + offset;
if (this.options.label) {
this.labelOffset = offset;
}
this.labelModule.draw(ctx, x, yLabel, selected, hover, 'hanging');
}
}
export default CircleImageBase;

View File

@@ -0,0 +1,295 @@
/**
* The Base class for all Nodes.
*/
class NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
this.body = body;
this.labelModule = labelModule;
this.setOptions(options);
this.top = undefined;
this.left = undefined;
this.height = undefined;
this.width = undefined;
this.radius = undefined;
this.margin = undefined;
this.refreshNeeded = true;
this.boundingBox = {top: 0, left: 0, right: 0, bottom: 0};
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
*
* @param {Label} labelModule
* @private
*/
_setMargins(labelModule) {
this.margin = {};
if (this.options.margin) {
if (typeof this.options.margin == 'object') {
this.margin.top = this.options.margin.top;
this.margin.right = this.options.margin.right;
this.margin.bottom = this.options.margin.bottom;
this.margin.left = this.options.margin.left;
} else {
this.margin.top = this.options.margin;
this.margin.right = this.options.margin;
this.margin.bottom = this.options.margin;
this.margin.left = this.options.margin;
}
}
labelModule.adjustSizes(this.margin)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} angle
* @returns {number}
* @private
*/
_distanceToBorder(ctx,angle) {
var borderWidth = this.options.borderWidth;
this.resize(ctx);
return Math.min(
Math.abs(this.width / 2 / Math.cos(angle)),
Math.abs(this.height / 2 / Math.sin(angle))) + borderWidth;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
enableShadow(ctx, values) {
if (values.shadow) {
ctx.shadowColor = values.shadowColor;
ctx.shadowBlur = values.shadowSize;
ctx.shadowOffsetX = values.shadowX;
ctx.shadowOffsetY = values.shadowY;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
disableShadow(ctx, values) {
if (values.shadow) {
ctx.shadowColor = 'rgba(0,0,0,0)';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
enableBorderDashes(ctx, values) {
if (values.borderDashes !== false) {
if (ctx.setLineDash !== undefined) {
let dashes = values.borderDashes;
if (dashes === true) {
dashes = [5,15]
}
ctx.setLineDash(dashes);
}
else {
console.warn("setLineDash is not supported in this browser. The dashed borders cannot be used.");
this.options.shapeProperties.borderDashes = false;
values.borderDashes = false;
}
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
disableBorderDashes(ctx, values) {
if (values.borderDashes !== false) {
if (ctx.setLineDash !== undefined) {
ctx.setLineDash([0]);
}
else {
console.warn("setLineDash is not supported in this browser. The dashed borders cannot be used.");
this.options.shapeProperties.borderDashes = false;
values.borderDashes = false;
}
}
}
/**
* Determine if the shape of a node needs to be recalculated.
*
* @param {boolean} selected
* @param {boolean} hover
* @returns {boolean}
* @protected
*/
needsRefresh(selected, hover) {
if (this.refreshNeeded === true) {
// This is probably not the best location to reset this member.
// However, in the current logic, it is the most convenient one.
this.refreshNeeded = false;
return true;
}
return (this.width === undefined) || (this.labelModule.differentState(selected, hover));
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
initContextForDraw(ctx, values) {
var borderWidth = values.borderWidth / this.body.view.scale;
ctx.lineWidth = Math.min(this.width, borderWidth);
ctx.strokeStyle = values.borderColor;
ctx.fillStyle = values.color;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
performStroke(ctx, values) {
var borderWidth = values.borderWidth / this.body.view.scale;
//draw dashed border if enabled, save and restore is required for firefox not to crash on unix.
ctx.save();
// if borders are zero width, they will be drawn with width 1 by default. This prevents that
if (borderWidth > 0) {
this.enableBorderDashes(ctx, values);
//draw the border
ctx.stroke();
//disable dashed border for other elements
this.disableBorderDashes(ctx, values);
}
ctx.restore();
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @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
*/
performFill(ctx, values) {
// draw shadow if enabled
this.enableShadow(ctx, values);
// draw the background
ctx.fill();
// disable shadows for other elements.
this.disableShadow(ctx, values);
this.performStroke(ctx, values);
}
/**
*
* @param {number} margin
* @private
*/
_addBoundingBoxMargin(margin) {
this.boundingBox.left -= margin;
this.boundingBox.top -= margin;
this.boundingBox.bottom += margin;
this.boundingBox.right += margin;
}
/**
* Actual implementation of this method call.
*
* Doing it like this makes it easier to override
* in the child classes.
*
* @param {number} x width
* @param {number} y height
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @private
*/
_updateBoundingBox(x, y, ctx, selected, hover) {
if (ctx !== undefined) {
this.resize(ctx, selected, hover);
}
this.left = x - this.width / 2;
this.top = y - this.height/ 2;
this.boundingBox.left = this.left;
this.boundingBox.top = this.top;
this.boundingBox.bottom = this.top + this.height;
this.boundingBox.right = this.left + this.width;
}
/**
* Default implementation of this method call.
* This acts as a stub which can be overridden.
*
* @param {number} x width
* @param {number} y height
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
*/
updateBoundingBox(x, y, ctx, selected, hover) {
this._updateBoundingBox(x, y, ctx, selected, hover);
}
/**
* Determine the dimensions to use for nodes with an internal label
*
* Currently, these are: Circle, Ellipse, Database, Box
* The other nodes have external labels, and will not call this method
*
* If there is no label, decent default values are supplied.
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
* @returns {{width:number, height:number}}
*/
getDimensionsFromLabel(ctx, selected, hover) {
// NOTE: previously 'textSize' was not put in 'this' for Ellipse
// TODO: examine the consequences.
this.textSize = this.labelModule.getTextSize(ctx, selected, hover);
var width = this.textSize.width;
var height = this.textSize.height;
const DEFAULT_SIZE = 14;
if (width === 0) {
// This happens when there is no label text set
width = DEFAULT_SIZE; // use a decent default
height = DEFAULT_SIZE; // if width zero, then height also always zero
}
return {width:width, height:height};
}
}
export default NodeBase;

View File

@@ -0,0 +1,85 @@
import NodeBase from '../util/NodeBase'
/**
* Base class for constructing Node/Cluster Shapes.
*
* @extends NodeBase
*/
class ShapeBase extends NodeBase {
/**
* @param {Object} options
* @param {Object} body
* @param {Label} labelModule
*/
constructor(options, body, labelModule) {
super(options, body, labelModule)
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} [selected]
* @param {boolean} [hover]
* @param {Object} [values={size: this.options.size}]
*/
resize(ctx, selected = this.selected, hover = this.hover, values = { size: this.options.size }) {
if (this.needsRefresh(selected, hover)) {
this.labelModule.getTextSize(ctx, selected, hover);
var size = 2 * values.size;
this.width = size;
this.height = size;
this.radius = 0.5*this.width;
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {string} shape
* @param {number} sizeMultiplier - Unused! TODO: Remove next major release
* @param {number} x
* @param {number} y
* @param {boolean} selected
* @param {boolean} hover
* @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
* @private
*/
_drawShape(ctx, shape, sizeMultiplier, x, y, selected, hover, values) {
this.resize(ctx, selected, hover, values);
this.left = x - this.width / 2;
this.top = y - this.height / 2;
this.initContextForDraw(ctx, values);
ctx[shape](x, y, values.size);
this.performFill(ctx, values);
if (this.options.label !== undefined) {
// Need to call following here in order to ensure value for `this.labelModule.size.height`
this.labelModule.calculateLabelSize(ctx, selected, hover, x, y, 'hanging')
let yLabel = y + 0.5 * this.height + 0.5 * this.labelModule.size.height;
this.labelModule.draw(ctx, x, yLabel, selected, hover, 'hanging');
}
this.updateBoundingBox(x,y);
}
/**
*
* @param {number} x
* @param {number} y
*/
updateBoundingBox(x, y) {
this.boundingBox.top = y - this.options.size;
this.boundingBox.left = x - this.options.size;
this.boundingBox.right = x + this.options.size;
this.boundingBox.bottom = y + this.options.size;
if (this.options.label !== undefined && this.labelModule.size.width > 0) {
this.boundingBox.left = Math.min(this.boundingBox.left, this.labelModule.size.left);
this.boundingBox.right = Math.max(this.boundingBox.right, this.labelModule.size.left + this.labelModule.size.width);
this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelModule.size.height);
}
}
}
export default ShapeBase;

View File

@@ -0,0 +1,494 @@
/**
* Barnes Hut Solver
*/
class BarnesHutSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.barnesHutTree;
this.setOptions(options);
this.randomSeed = 5;
// debug: show grid
// this.body.emitter.on("afterDrawing", (ctx) => {this._debug(ctx,'#ff0000')})
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
this.thetaInversed = 1 / this.options.theta;
// if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius
this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1, this.options.avoidOverlap));
}
/**
*
* @returns {number} random integer
*/
seededRandom() {
var x = Math.sin(this.randomSeed++) * 10000;
return x - Math.floor(x);
}
/**
* This function calculates the forces the nodes apply on each other based on a gravitational model.
* The Barnes Hut method is used to speed up this N-body simulation.
*
* @private
*/
solve() {
if (this.options.gravitationalConstant !== 0 && this.physicsBody.physicsNodeIndices.length > 0) {
let node;
let nodes = this.body.nodes;
let nodeIndices = this.physicsBody.physicsNodeIndices;
let nodeCount = nodeIndices.length;
// create the tree
let barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
// for debugging
this.barnesHutTree = barnesHutTree;
// place the nodes one by one recursively
for (let i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
// starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContributions(barnesHutTree.root, node);
}
}
}
}
/**
* @param {Object} parentBranch
* @param {Node} node
* @private
*/
_getForceContributions(parentBranch, node) {
this._getForceContribution(parentBranch.children.NW, node);
this._getForceContribution(parentBranch.children.NE, node);
this._getForceContribution(parentBranch.children.SW, node);
this._getForceContribution(parentBranch.children.SE, node);
}
/**
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
* If a region contains a single node, we check if it is not itself, then we apply the force.
*
* @param {Object} parentBranch
* @param {Node} node
* @private
*/
_getForceContribution(parentBranch, node) {
// we get no force contribution from an empty region
if (parentBranch.childrenCount > 0) {
let dx, dy, distance;
// get the distance from the center of mass to the node.
dx = parentBranch.centerOfMass.x - node.x;
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
// BarnesHutSolver condition
// original condition : s/d < theta = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.thetaInversed) {
this._calculateForces(distance, dx, dy, node, parentBranch);
}
else {
// Did not pass the condition, go into children if available
if (parentBranch.childrenCount === 4) {
this._getForceContributions(parentBranch, node);
}
else { // parentBranch must have only one node, if it was empty we wouldnt be here
if (parentBranch.children.data.id != node.id) { // if it is not self
this._calculateForces(distance, dx, dy, node, parentBranch);
}
}
}
}
}
/**
* Calculate the forces based on the distance.
*
* @param {number} distance
* @param {number} dx
* @param {number} dy
* @param {Node} node
* @param {Object} parentBranch
* @private
*/
_calculateForces(distance, dx, dy, node, parentBranch) {
if (distance === 0) {
distance = 0.1;
dx = distance;
}
if (this.overlapAvoidanceFactor < 1 && node.shape.radius) {
distance = Math.max(0.1 + (this.overlapAvoidanceFactor * node.shape.radius), distance - node.shape.radius);
}
// the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines
// it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce
let gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance,3);
let fx = dx * gravityForce;
let fy = dy * gravityForce;
this.physicsBody.forces[node.id].x += fx;
this.physicsBody.forces[node.id].y += fy;
}
/**
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
*
* @param {Array.<Node>} nodes
* @param {Array.<number>} nodeIndices
* @returns {{root: {centerOfMass: {x: number, y: number}, mass: number, range: {minX: number, maxX: number, minY: number, maxY: number}, size: number, calcSize: number, children: {data: null}, maxWidth: number, level: number, childrenCount: number}}} BarnesHutTree
* @private
*/
_formBarnesHutTree(nodes, nodeIndices) {
let node;
let nodeCount = nodeIndices.length;
let minX = nodes[nodeIndices[0]].x;
let minY = nodes[nodeIndices[0]].y;
let maxX = nodes[nodeIndices[0]].x;
let maxY = nodes[nodeIndices[0]].y;
// get the range of the nodes
for (let i = 1; i < nodeCount; i++) {
let node = nodes[nodeIndices[i]];
let x = node.x;
let y = node.y;
if (node.options.mass > 0) {
if (x < minX) {
minX = x;
}
if (x > maxX) {
maxX = x;
}
if (y < minY) {
minY = y;
}
if (y > maxY) {
maxY = y;
}
}
}
// make the range a square
let sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
if (sizeDiff > 0) {
minY -= 0.5 * sizeDiff;
maxY += 0.5 * sizeDiff;
} // xSize > ySize
else {
minX += 0.5 * sizeDiff;
maxX -= 0.5 * sizeDiff;
} // xSize < ySize
let minimumTreeSize = 1e-5;
let rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX));
let halfRootSize = 0.5 * rootSize;
let centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
// construct the barnesHutTree
let barnesHutTree = {
root: {
centerOfMass: {x: 0, y: 0},
mass: 0,
range: {
minX: centerX - halfRootSize, maxX: centerX + halfRootSize,
minY: centerY - halfRootSize, maxY: centerY + halfRootSize
},
size: rootSize,
calcSize: 1 / rootSize,
children: {data: null},
maxWidth: 0,
level: 0,
childrenCount: 4
}
};
this._splitBranch(barnesHutTree.root);
// place the nodes one by one recursively
for (let i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
this._placeInTree(barnesHutTree.root, node);
}
}
// make global
return barnesHutTree
}
/**
* this updates the mass of a branch. this is increased by adding a node.
*
* @param {Object} parentBranch
* @param {Node} node
* @private
*/
_updateBranchMass(parentBranch, node) {
let centerOfMass = parentBranch.centerOfMass;
let totalMass = parentBranch.mass + node.options.mass;
let totalMassInv = 1 / totalMass;
centerOfMass.x = centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
centerOfMass.x *= totalMassInv;
centerOfMass.y = centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
centerOfMass.y *= totalMassInv;
parentBranch.mass = totalMass;
let biggestSize = Math.max(Math.max(node.height, node.radius), node.width);
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
}
/**
* determine in which branch the node will be placed.
*
* @param {Object} parentBranch
* @param {Node} node
* @param {boolean} skipMassUpdate
* @private
*/
_placeInTree(parentBranch, node, skipMassUpdate) {
if (skipMassUpdate != true || skipMassUpdate === undefined) {
// update the mass of the branch.
this._updateBranchMass(parentBranch, node);
}
let range = parentBranch.children.NW.range;
let region;
if (range.maxX > node.x) { // in NW or SW
if (range.maxY > node.y) {
region = "NW";
}
else {
region = "SW";
}
}
else { // in NE or SE
if (range.maxY > node.y) {
region = "NE";
}
else {
region = "SE";
}
}
this._placeInRegion(parentBranch, node, region);
}
/**
* actually place the node in a region (or branch)
*
* @param {Object} parentBranch
* @param {Node} node
* @param {'NW'| 'NE' | 'SW' | 'SE'} region
* @private
*/
_placeInRegion(parentBranch, node, region) {
let children = parentBranch.children[region];
switch (children.childrenCount) {
case 0: // place node here
children.children.data = node;
children.childrenCount = 1;
this._updateBranchMass(children, node);
break;
case 1: // convert into children
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
// we move one node a little bit and we do not put it in the tree.
if (children.children.data.x === node.x && children.children.data.y === node.y) {
node.x += this.seededRandom();
node.y += this.seededRandom();
}
else {
this._splitBranch(children);
this._placeInTree(children, node);
}
break;
case 4: // place in branch
this._placeInTree(children, node);
break;
}
}
/**
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
* after the split is complete.
*
* @param {Object} parentBranch
* @private
*/
_splitBranch(parentBranch) {
// if the branch is shaded with a node, replace the node in the new subset.
let containedNode = null;
if (parentBranch.childrenCount === 1) {
containedNode = parentBranch.children.data;
parentBranch.mass = 0;
parentBranch.centerOfMass.x = 0;
parentBranch.centerOfMass.y = 0;
}
parentBranch.childrenCount = 4;
parentBranch.children.data = null;
this._insertRegion(parentBranch, "NW");
this._insertRegion(parentBranch, "NE");
this._insertRegion(parentBranch, "SW");
this._insertRegion(parentBranch, "SE");
if (containedNode != null) {
this._placeInTree(parentBranch, containedNode);
}
}
/**
* This function subdivides the region into four new segments.
* Specifically, this inserts a single new segment.
* It fills the children section of the parentBranch
*
* @param {Object} parentBranch
* @param {'NW'| 'NE' | 'SW' | 'SE'} region
* @private
*/
_insertRegion(parentBranch, region) {
let minX, maxX, minY, maxY;
let childSize = 0.5 * parentBranch.size;
switch (region) {
case "NW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "NE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY;
maxY = parentBranch.range.minY + childSize;
break;
case "SW":
minX = parentBranch.range.minX;
maxX = parentBranch.range.minX + childSize;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
case "SE":
minX = parentBranch.range.minX + childSize;
maxX = parentBranch.range.maxX;
minY = parentBranch.range.minY + childSize;
maxY = parentBranch.range.maxY;
break;
}
parentBranch.children[region] = {
centerOfMass: {x: 0, y: 0},
mass: 0,
range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY},
size: 0.5 * parentBranch.size,
calcSize: 2 * parentBranch.calcSize,
children: {data: null},
maxWidth: 0,
level: parentBranch.level + 1,
childrenCount: 0
};
}
//--------------------------- DEBUGGING BELOW ---------------------------//
/**
* This function is for debugging purposed, it draws the tree.
*
* @param {CanvasRenderingContext2D} ctx
* @param {string} color
* @private
*/
_debug(ctx, color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;
this._drawBranch(this.barnesHutTree.root, ctx, color);
}
}
/**
* This function is for debugging purposes. It draws the branches recursively.
*
* @param {Object} branch
* @param {CanvasRenderingContext2D} ctx
* @param {string} color
* @private
*/
_drawBranch(branch, ctx, color) {
if (color === undefined) {
color = "#FF0000";
}
if (branch.childrenCount === 4) {
this._drawBranch(branch.children.NW, ctx);
this._drawBranch(branch.children.NE, ctx);
this._drawBranch(branch.children.SE, ctx);
this._drawBranch(branch.children.SW, ctx);
}
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(branch.range.minX, branch.range.minY);
ctx.lineTo(branch.range.maxX, branch.range.minY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX, branch.range.minY);
ctx.lineTo(branch.range.maxX, branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.maxX, branch.range.maxY);
ctx.lineTo(branch.range.minX, branch.range.maxY);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(branch.range.minX, branch.range.maxY);
ctx.lineTo(branch.range.minX, branch.range.minY);
ctx.stroke();
/*
if (branch.mass > 0) {
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
ctx.stroke();
}
*/
}
}
export default BarnesHutSolver;

View File

@@ -0,0 +1,61 @@
/**
* Central Gravity Solver
*/
class CentralGravitySolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
* Calculates forces for each node
*/
solve() {
let dx, dy, distance, node;
let nodes = this.body.nodes;
let nodeIndices = this.physicsBody.physicsNodeIndices;
let forces = this.physicsBody.forces;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
node = nodes[nodeId];
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);
this._calculateForces(distance, dx, dy, forces, node);
}
}
/**
* Calculate the forces based on the distance.
* @param {number} distance
* @param {number} dx
* @param {number} dy
* @param {Object<Node.id, vis.Node>} forces
* @param {Node} node
* @private
*/
_calculateForces(distance, dx, dy, forces, node) {
let gravityForce = (distance === 0) ? 0 : (this.options.centralGravity / distance);
forces[node.id].x = dx * gravityForce;
forces[node.id].y = dy * gravityForce;
}
}
export default CentralGravitySolver;

View File

@@ -0,0 +1,37 @@
import CentralGravitySolver from "./CentralGravitySolver"
/**
* @extends CentralGravitySolver
*/
class ForceAtlas2BasedCentralGravitySolver extends CentralGravitySolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
super(body, physicsBody, options);
}
/**
* Calculate the forces based on the distance.
*
* @param {number} distance
* @param {number} dx
* @param {number} dy
* @param {Object<Node.id, Node>} forces
* @param {Node} node
* @private
*/
_calculateForces(distance, dx, dy, forces, node) {
if (distance > 0) {
let degree = (node.edges.length + 1);
let gravityForce = this.options.centralGravity * degree * node.options.mass;
forces[node.id].x = dx * gravityForce;
forces[node.id].y = dy * gravityForce;
}
}
}
export default ForceAtlas2BasedCentralGravitySolver;

View File

@@ -0,0 +1,48 @@
import BarnesHutSolver from "./BarnesHutSolver"
/**
* @extends BarnesHutSolver
*/
class ForceAtlas2BasedRepulsionSolver extends BarnesHutSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
super(body, physicsBody, options);
}
/**
* Calculate the forces based on the distance.
*
* @param {number} distance
* @param {number} dx
* @param {number} dy
* @param {Node} node
* @param {Object} parentBranch
* @private
*/
_calculateForces(distance, dx, dy, node, parentBranch) {
if (distance === 0) {
distance = 0.1 * Math.random();
dx = distance;
}
if (this.overlapAvoidanceFactor < 1 && node.shape.radius) {
distance = Math.max(0.1 + (this.overlapAvoidanceFactor * node.shape.radius), distance - node.shape.radius);
}
let degree = (node.edges.length + 1);
// the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines
// it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce
let gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass * degree / Math.pow(distance,2);
let fx = dx * gravityForce;
let fy = dy * gravityForce;
this.physicsBody.forces[node.id].x += fx;
this.physicsBody.forces[node.id].y += fy;
}
}
export default ForceAtlas2BasedRepulsionSolver;

View File

@@ -0,0 +1,81 @@
/**
* Hierarchical Repulsion Solver
*/
class HierarchicalRepulsionSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
solve() {
var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j;
var nodes = this.body.nodes;
var nodeIndices = this.physicsBody.physicsNodeIndices;
var forces = this.physicsBody.forces;
// repulsing forces between nodes
var nodeDistance = this.options.nodeDistance;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j
for (i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
// nodes only affect nodes on their level
if (node1.level === node2.level) {
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
var steepness = 0.05;
if (distance < nodeDistance) {
repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2);
}
else {
repulsingForce = 0;
}
// normalize force with
if (distance === 0) {
distance = 0.01;
}
else {
repulsingForce = repulsingForce / distance;
}
fx = dx * repulsingForce;
fy = dy * repulsingForce;
forces[node1.id].x -= fx;
forces[node1.id].y -= fy;
forces[node2.id].x += fx;
forces[node2.id].y += fy;
}
}
}
}
}
export default HierarchicalRepulsionSolver;

View File

@@ -0,0 +1,118 @@
/**
* Hierarchical Spring Solver
*/
class HierarchicalSpringSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
solve() {
var edgeLength, edge;
var dx, dy, fx, fy, springForce, distance;
var edges = this.body.edges;
var factor = 0.5;
var edgeIndices = this.physicsBody.physicsEdgeIndices;
var nodeIndices = this.physicsBody.physicsNodeIndices;
var forces = this.physicsBody.forces;
// initialize the spring force counters
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
forces[nodeId].springFx = 0;
forces[nodeId].springFy = 0;
}
// forces caused by the edges, modelled as springs
for (let i = 0; i < edgeIndices.length; i++) {
edge = edges[edgeIndices[i]];
if (edge.connected === true) {
edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
distance = Math.sqrt(dx * dx + dy * dy);
distance = distance === 0 ? 0.01 : distance;
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
springForce = this.options.springConstant * (edgeLength - distance) / distance;
fx = dx * springForce;
fy = dy * springForce;
if (edge.to.level != edge.from.level) {
if (forces[edge.toId] !== undefined) {
forces[edge.toId].springFx -= fx;
forces[edge.toId].springFy -= fy;
}
if (forces[edge.fromId] !== undefined) {
forces[edge.fromId].springFx += fx;
forces[edge.fromId].springFy += fy;
}
}
else {
if (forces[edge.toId] !== undefined) {
forces[edge.toId].x -= factor * fx;
forces[edge.toId].y -= factor * fy;
}
if (forces[edge.fromId] !== undefined) {
forces[edge.fromId].x += factor * fx;
forces[edge.fromId].y += factor * fy;
}
}
}
}
// normalize spring forces
springForce = 1;
var springFx, springFy;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
springFx = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFx));
springFy = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFy));
forces[nodeId].x += springFx;
forces[nodeId].y += springFy;
}
// retain energy balance
var totalFx = 0;
var totalFy = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
totalFx += forces[nodeId].x;
totalFy += forces[nodeId].y;
}
var correctionFx = totalFx / nodeIndices.length;
var correctionFy = totalFy / nodeIndices.length;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
forces[nodeId].x -= correctionFx;
forces[nodeId].y -= correctionFy;
}
}
}
export default HierarchicalSpringSolver;

View File

@@ -0,0 +1,84 @@
/**
* Repulsion Solver
*/
class RepulsionSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
* Calculate the forces the nodes apply on each other based on a repulsion field.
* This field is linearly approximated.
*
* @private
*/
solve() {
var dx, dy, distance, fx, fy, repulsingForce, node1, node2;
var nodes = this.body.nodes;
var nodeIndices = this.physicsBody.physicsNodeIndices;
var forces = this.physicsBody.forces;
// repulsing forces between nodes
var nodeDistance = this.options.nodeDistance;
// approximation constants
var a = (-2 / 3) / nodeDistance;
var b = 4 / 3;
// we loop from i over all but the last entree in the array
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i === j
for (let i = 0; i < nodeIndices.length - 1; i++) {
node1 = nodes[nodeIndices[i]];
for (let j = i + 1; j < nodeIndices.length; j++) {
node2 = nodes[nodeIndices[j]];
dx = node2.x - node1.x;
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
// same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
if (distance === 0) {
distance = 0.1*Math.random();
dx = distance;
}
if (distance < 2 * nodeDistance) {
if (distance < 0.5 * nodeDistance) {
repulsingForce = 1.0;
}
else {
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness))
}
repulsingForce = repulsingForce / distance;
fx = dx * repulsingForce;
fy = dy * repulsingForce;
forces[node1.id].x -= fx;
forces[node1.id].y -= fy;
forces[node2.id].x += fx;
forces[node2.id].y += fy;
}
}
}
}
}
export default RepulsionSolver;

View File

@@ -0,0 +1,94 @@
/**
* Spring Solver
*/
class SpringSolver {
/**
* @param {Object} body
* @param {{physicsNodeIndices: Array, physicsEdgeIndices: Array, forces: {}, velocities: {}}} physicsBody
* @param {Object} options
*/
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
/**
*
* @param {Object} options
*/
setOptions(options) {
this.options = options;
}
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
solve() {
let edgeLength, edge;
let edgeIndices = this.physicsBody.physicsEdgeIndices;
let edges = this.body.edges;
let node1, node2, node3;
// forces caused by the edges, modelled as springs
for (let i = 0; i < edgeIndices.length; i++) {
edge = edges[edgeIndices[i]];
if (edge.connected === true && edge.toId !== edge.fromId) {
// only calculate forces if nodes are in the same sector
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
if (edge.edgeType.via !== undefined) {
edgeLength = edge.options.length === undefined ? this.options.springLength : edge.options.length;
node1 = edge.to;
node2 = edge.edgeType.via;
node3 = edge.from;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
else {
// the * 1.5 is here so the edge looks as large as a smooth edge. It does not initially because the smooth edges use
// the support nodes which exert a repulsive force on the to and from nodes, making the edge appear larger.
edgeLength = edge.options.length === undefined ? this.options.springLength * 1.5: edge.options.length;
this._calculateSpringForce(edge.from, edge.to, edgeLength);
}
}
}
}
}
/**
* This is the code actually performing the calculation for the function above.
*
* @param {Node} node1
* @param {Node} node2
* @param {number} edgeLength
* @private
*/
_calculateSpringForce(node1, node2, edgeLength) {
let dx = (node1.x - node2.x);
let dy = (node1.y - node2.y);
let distance = Math.max(Math.sqrt(dx * dx + dy * dy),0.01);
// the 1/distance is so the fx and fy can be calculated without sine or cosine.
let springForce = this.options.springConstant * (edgeLength - distance) / distance;
let fx = dx * springForce;
let fy = dy * springForce;
// handle the case where one node is not part of the physcis
if (this.physicsBody.forces[node1.id] !== undefined) {
this.physicsBody.forces[node1.id].x += fx;
this.physicsBody.forces[node1.id].y += fy;
}
if (this.physicsBody.forces[node2.id] !== undefined) {
this.physicsBody.forces[node2.id].x -= fx;
this.physicsBody.forces[node2.id].y -= fy;
}
}
}
export default SpringSolver;

View File

@@ -0,0 +1,139 @@
/**
* Definitions for param's in jsdoc.
* These are more or less global within Network. Putting them here until I can figure out
* where to really put them
*
* @typedef {string|number} Id
* @typedef {Id} NodeId
* @typedef {Id} EdgeId
* @typedef {Id} LabelId
*
* @typedef {{x: number, y: number}} point
* @typedef {{left: number, top: number, width: number, height: number}} rect
* @typedef {{x: number, y:number, angle: number}} rotationPoint
* - point to rotate around and the angle in radians to rotate. angle == 0 means no rotation
* @typedef {{nodeId:NodeId}} nodeClickItem
* @typedef {{nodeId:NodeId, labelId:LabelId}} nodeLabelClickItem
* @typedef {{edgeId:EdgeId}} edgeClickItem
* @typedef {{edgeId:EdgeId, labelId:LabelId}} edgeLabelClickItem
*/
let util = require("../../../../util");
/**
* Helper functions for components
* @class
*/
class ComponentUtil {
/**
* Determine values to use for (sub)options of 'chosen'.
*
* This option is either a boolean or an object whose values should be examined further.
* The relevant structures are:
*
* - chosen: <boolean value>
* - chosen: { subOption: <boolean or function> }
*
* Where subOption is 'node', 'edge' or 'label'.
*
* The intention of this method appears to be to set a specific priority to the options;
* Since most properties are either bridged or merged into the local options objects, there
* is not much point in handling them separately.
* TODO: examine if 'most' in previous sentence can be replaced with 'all'. In that case, we
* should be able to get rid of this method.
*
* @param {string} subOption option within object 'chosen' to consider; either 'node', 'edge' or 'label'
* @param {Object} pile array of options objects to consider
*
* @return {boolean|function} value for passed subOption of 'chosen' to use
*/
static choosify(subOption, pile) {
// allowed values for subOption
let allowed = [ 'node', 'edge', 'label'];
let value = true;
let chosen = util.topMost(pile, 'chosen');
if (typeof chosen === 'boolean') {
value = chosen;
} else if (typeof chosen === 'object') {
if (allowed.indexOf(subOption) === -1 ) {
throw new Error('choosify: subOption \'' + subOption + '\' should be one of '
+ "'" + allowed.join("', '") + "'");
}
let chosenEdge = util.topMost(pile, ['chosen', subOption]);
if ((typeof chosenEdge === 'boolean') || (typeof chosenEdge === 'function')) {
value = chosenEdge;
}
}
return value;
}
/**
* Check if the point falls within the given rectangle.
*
* @param {rect} rect
* @param {point} point
* @param {rotationPoint} [rotationPoint] if specified, the rotation that applies to the rectangle.
* @returns {boolean} true if point within rectangle, false otherwise
* @static
*/
static pointInRect(rect, point, rotationPoint) {
if (rect.width <= 0 || rect.height <= 0) {
return false; // early out
}
if (rotationPoint !== undefined) {
// Rotate the point the same amount as the rectangle
var tmp = {
x: point.x - rotationPoint.x,
y: point.y - rotationPoint.y
};
if (rotationPoint.angle !== 0) {
// In order to get the coordinates the same, you need to
// rotate in the reverse direction
var angle = -rotationPoint.angle;
var tmp2 = {
x: Math.cos(angle)*tmp.x - Math.sin(angle)*tmp.y,
y: Math.sin(angle)*tmp.x + Math.cos(angle)*tmp.y
};
point = tmp2;
} else {
point = tmp;
}
// Note that if a rotation is specified, the rectangle coordinates
// are **not* the full canvas coordinates. They are relative to the
// rotationPoint. Hence, the point coordinates need not be translated
// back in this case.
}
var right = rect.x + rect.width;
var bottom = rect.y + rect.width;
return (
rect.left < point.x &&
right > point.x &&
rect.top < point.y &&
bottom > point.y
);
}
/**
* Check if given value is acceptable as a label text.
*
* @param {*} text value to check; can be anything at this point
* @returns {boolean} true if valid label value, false otherwise
*/
static isValidLabel(text) {
// Note that this is quite strict: types that *might* be converted to string are disallowed
return (typeof text === 'string' && text !== '');
}
}
export default ComponentUtil;

View File

@@ -0,0 +1,799 @@
let util = require('../../../../util');
let ComponentUtil = require('./ComponentUtil').default;
let LabelSplitter = require('./LabelSplitter').default;
/**
* @typedef {'bold'|'ital'|'boldital'|'mono'|'normal'} MultiFontStyle
*
* The allowed specifiers of multi-fonts.
*/
/**
* @typedef {{color:string, size:number, face:string, mod:string, vadjust:number}} MultiFontOptions
*
* The full set of options of a given multi-font.
*/
/**
* @typedef {Array.<object>} Pile
*
* Sequence of option objects, the order is significant.
* The sequence is used to determine the value of a given option.
*
* Usage principles:
*
* - All search is done in the sequence of the pile.
* - As soon as a value is found, the searching stops.
* - prototypes are totally ignored. The idea is to add option objects used as prototypes
* to the pile, in the correct order.
*/
/**
* List of special styles for multi-fonts
* @private
*/
const multiFontStyle = ['bold', 'ital', 'boldital', 'mono'];
/**
* A Label to be used for Nodes or Edges.
*/
class Label {
/**
* @param {Object} body
* @param {Object} options
* @param {boolean} [edgelabel=false]
*/
constructor(body, options, edgelabel = false) {
this.body = body;
this.pointToSelf = false;
this.baseSize = undefined;
this.fontOptions = {}; // instance variable containing the *instance-local* font options
this.setOptions(options);
this.size = {top: 0, left: 0, width: 0, height: 0, yLine: 0};
this.isEdgeLabel = edgelabel;
}
/**
* @param {Object} options the options of the parent Node-instance
*/
setOptions(options) {
this.elementOptions = options; // Reference to the options of the parent Node-instance
this.initFontOptions(options.font);
if (ComponentUtil.isValidLabel(options.label)) {
this.labelDirty = true;
} else {
// Bad label! Change the option value to prevent bad stuff happening
options.label = '';
}
if (options.font !== undefined && options.font !== null) { // font options can be deleted at various levels
if (typeof options.font === 'string') {
this.baseSize = this.fontOptions.size;
}
else if (typeof options.font === 'object') {
let size = options.font.size;
if (size !== undefined) {
this.baseSize = size;
}
}
}
}
/**
* Init the font Options structure.
*
* Member fontOptions serves as an accumulator for the current font options.
* As such, it needs to be completely separated from the node options.
*
* @param {Object} newFontOptions the new font options to process
* @private
*/
initFontOptions(newFontOptions) {
// Prepare the multi-font option objects.
// These will be filled in propagateFonts(), if required
util.forEach(multiFontStyle, (style) => {
this.fontOptions[style] = {};
});
// Handle shorthand option, if present
if (Label.parseFontString(this.fontOptions, newFontOptions)) {
this.fontOptions.vadjust = 0;
return;
}
// Copy over the non-multifont options, if specified
util.forEach(newFontOptions, (prop, n) => {
if (prop !== undefined && prop !== null && typeof prop !== 'object') {
this.fontOptions[n] = prop;
}
});
}
/**
* If in-variable is a string, parse it as a font specifier.
*
* Note that following is not done here and have to be done after the call:
* - No number conversion (size)
* - Not all font options are set (vadjust, mod)
*
* @param {Object} outOptions out-parameter, object in which to store the parse results (if any)
* @param {Object} inOptions font options to parse
* @return {boolean} true if font parsed as string, false otherwise
* @static
*/
static parseFontString(outOptions, inOptions) {
if (!inOptions || typeof inOptions !== 'string') return false;
let newOptionsArray = inOptions.split(" ");
outOptions.size = newOptionsArray[0].replace("px",'');
outOptions.face = newOptionsArray[1];
outOptions.color = newOptionsArray[2];
return true;
}
/**
* Set the width and height constraints based on 'nearest' value
*
* @param {Array} pile array of option objects to consider
* @returns {object} the actual constraint values to use
* @private
*/
constrain(pile) {
// NOTE: constrainWidth and constrainHeight never set!
// NOTE: for edge labels, only 'maxWdt' set
// Node labels can set all the fields
let fontOptions = {
constrainWidth: false,
maxWdt: -1,
minWdt: -1,
constrainHeight: false,
minHgt: -1,
valign: 'middle',
}
let widthConstraint = util.topMost(pile, 'widthConstraint');
if (typeof widthConstraint === 'number') {
fontOptions.maxWdt = Number(widthConstraint);
fontOptions.minWdt = Number(widthConstraint);
} else if (typeof widthConstraint === 'object') {
let widthConstraintMaximum = util.topMost(pile, ['widthConstraint', 'maximum']);
if (typeof widthConstraintMaximum === 'number') {
fontOptions.maxWdt = Number(widthConstraintMaximum);
}
let widthConstraintMinimum = util.topMost(pile, ['widthConstraint', 'minimum'])
if (typeof widthConstraintMinimum === 'number') {
fontOptions.minWdt = Number(widthConstraintMinimum);
}
}
let heightConstraint = util.topMost(pile, 'heightConstraint');
if (typeof heightConstraint === 'number') {
fontOptions.minHgt = Number(heightConstraint);
} else if (typeof heightConstraint === 'object') {
let heightConstraintMinimum = util.topMost(pile, ['heightConstraint', 'minimum']);
if (typeof heightConstraintMinimum === 'number') {
fontOptions.minHgt = Number(heightConstraintMinimum);
}
let heightConstraintValign = util.topMost(pile, ['heightConstraint', 'valign']);
if (typeof heightConstraintValign === 'string') {
if ((heightConstraintValign === 'top')|| (heightConstraintValign === 'bottom')) {
fontOptions.valign = heightConstraintValign;
}
}
}
return fontOptions;
}
/**
* Set options and update internal state
*
* @param {Object} options options to set
* @param {Array} pile array of option objects to consider for option 'chosen'
*/
update(options, pile) {
this.setOptions(options, true);
this.propagateFonts(pile);
util.deepExtend(this.fontOptions, this.constrain(pile));
this.fontOptions.chooser = ComponentUtil.choosify('label', pile);
}
/**
* When margins are set in an element, adjust sizes is called to remove them
* from the width/height constraints. This must be done prior to label sizing.
*
* @param {{top: number, right: number, bottom: number, left: number}} margins
*/
adjustSizes(margins) {
let widthBias = (margins) ? (margins.right + margins.left) : 0;
if (this.fontOptions.constrainWidth) {
this.fontOptions.maxWdt -= widthBias;
this.fontOptions.minWdt -= widthBias;
}
let heightBias = (margins) ? (margins.top + margins.bottom) : 0;
if (this.fontOptions.constrainHeight) {
this.fontOptions.minHgt -= heightBias;
}
}
/////////////////////////////////////////////////////////
// Methods for handling options piles
// Eventually, these will be moved to a separate class
/////////////////////////////////////////////////////////
/**
* Add the font members of the passed list of option objects to the pile.
*
* @param {Pile} dstPile pile of option objects add to
* @param {Pile} srcPile pile of option objects to take font options from
* @private
*/
addFontOptionsToPile(dstPile, srcPile) {
for (let i = 0; i < srcPile.length; ++i) {
this.addFontToPile(dstPile, srcPile[i]);
}
}
/**
* Add given font option object to the list of objects (the 'pile') to consider for determining
* multi-font option values.
*
* @param {Pile} pile pile of option objects to use
* @param {object} options instance to add to pile
* @private
*/
addFontToPile(pile, options) {
if (options === undefined) return;
if (options.font === undefined || options.font === null) return;
let item = options.font;
pile.push(item);
}
/**
* Collect all own-property values from the font pile that aren't multi-font option objectss.
*
* @param {Pile} pile pile of option objects to use
* @returns {object} object with all current own basic font properties
* @private
*/
getBasicOptions(pile) {
let ret = {};
// Scans the whole pile to get all options present
for (let n = 0; n < pile.length; ++n) {
let fontOptions = pile[n];
// Convert shorthand if necessary
let tmpShorthand = {};
if (Label.parseFontString(tmpShorthand, fontOptions)) {
fontOptions = tmpShorthand;
}
util.forEach(fontOptions, (opt, name) => {
if (opt === undefined) return; // multi-font option need not be present
if (ret.hasOwnProperty(name)) return; // Keep first value we encounter
if (multiFontStyle.indexOf(name) !== -1) {
// Skip multi-font properties but we do need the structure
ret[name] = {};
} else {
ret[name] = opt;
}
});
}
return ret;
}
/**
* Return the value for given option for the given multi-font.
*
* All available option objects are trawled in the set order to construct the option values.
*
* ---------------------------------------------------------------------
* ## Traversal of pile for multi-fonts
*
* The determination of multi-font option values is a special case, because any values not
* present in the multi-font options should by definition be taken from the main font options,
* i.e. from the current 'parent' object of the multi-font option.
*
* ### Search order for multi-fonts
*
* 'bold' used as example:
*
* - search in option group 'bold' in local properties
* - search in main font option group in local properties
*
* ---------------------------------------------------------------------
*
* @param {Pile} pile pile of option objects to use
* @param {MultiFontStyle} multiName sub path for the multi-font
* @param {string} option the option to search for, for the given multi-font
* @returns {string|number} the value for the given option
* @private
*/
getFontOption(pile, multiName, option) {
let multiFont;
// Search multi font in local properties
for (let n = 0; n < pile.length; ++n) {
let fontOptions = pile[n];
if (fontOptions.hasOwnProperty(multiName)) {
multiFont = fontOptions[multiName];
if (multiFont === undefined || multiFont === null) continue;
// Convert shorthand if necessary
// TODO: inefficient to do this conversion every time; find a better way.
let tmpShorthand = {};
if (Label.parseFontString(tmpShorthand, multiFont)) {
multiFont = tmpShorthand;
}
if (multiFont.hasOwnProperty(option)) {
return multiFont[option];
}
}
}
// Option is not mentioned in the multi font options; take it from the parent font options.
// These have already been converted with getBasicOptions(), so use the converted values.
if (this.fontOptions.hasOwnProperty(option)) {
return this.fontOptions[option];
}
// A value **must** be found; you should never get here.
throw new Error("Did not find value for multi-font for property: '" + option + "'");
}
/**
* Return all options values for the given multi-font.
*
* All available option objects are trawled in the set order to construct the option values.
*
* @param {Pile} pile pile of option objects to use
* @param {MultiFontStyle} multiName sub path for the mod-font
* @returns {MultiFontOptions}
* @private
*/
getFontOptions(pile, multiName) {
let result = {};
let optionNames = ['color', 'size', 'face', 'mod', 'vadjust']; // List of allowed options per multi-font
for (let i = 0; i < optionNames.length; ++i) {
let mod = optionNames[i];
result[mod] = this.getFontOption(pile, multiName, mod);
}
return result;
}
/////////////////////////////////////////////////////////
// End methods for handling options piles
/////////////////////////////////////////////////////////
/**
* Collapse the font options for the multi-font to single objects, from
* the chain of option objects passed (the 'pile').
*
* @param {Pile} pile sequence of option objects to consider.
* First item in list assumed to be the newly set options.
*/
propagateFonts(pile) {
let fontPile = []; // sequence of font objects to consider, order important
// Note that this.elementOptions is not used here.
this.addFontOptionsToPile(fontPile, pile);
this.fontOptions = this.getBasicOptions(fontPile);
// We set multifont values even if multi === false, for consistency (things break otherwise)
for (let i = 0; i < multiFontStyle.length; ++i) {
let mod = multiFontStyle[i];
let modOptions = this.fontOptions[mod];
let tmpMultiFontOptions = this.getFontOptions(fontPile, mod);
// Copy over found values
util.forEach(tmpMultiFontOptions, (option, n) => {
modOptions[n] = option;
});
modOptions.size = Number(modOptions.size);
modOptions.vadjust = Number(modOptions.vadjust);
}
}
/**
* Main function. This is called from anything that wants to draw a label.
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {boolean} selected
* @param {boolean} hover
* @param {string} [baseline='middle']
*/
draw(ctx, x, y, selected, hover, baseline = 'middle') {
// if no label, return
if (this.elementOptions.label === undefined)
return;
// check if we have to render the label
let viewFontSize = this.fontOptions.size * this.body.view.scale;
if (this.elementOptions.label && viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1)
return;
// This ensures that there will not be HUGE letters on screen
// by setting an upper limit on the visible text size (regardless of zoomLevel)
if (viewFontSize >= this.elementOptions.scaling.label.maxVisible) {
viewFontSize = Number(this.elementOptions.scaling.label.maxVisible) / this.body.view.scale;
}
// update the size cache if required
this.calculateLabelSize(ctx, selected, hover, x, y, baseline);
this._drawBackground(ctx);
this._drawText(ctx, x, this.size.yLine, baseline, viewFontSize);
}
/**
* Draws the label background
* @param {CanvasRenderingContext2D} ctx
* @private
*/
_drawBackground(ctx) {
if (this.fontOptions.background !== undefined && this.fontOptions.background !== "none") {
ctx.fillStyle = this.fontOptions.background;
let size = this.getSize();
ctx.fillRect(size.left, size.top, size.width, size.height);
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {string} [baseline='middle']
* @param {number} viewFontSize
* @private
*/
_drawText(ctx, x, y, baseline = 'middle', viewFontSize) {
[x, y] = this._setAlignment(ctx, x, y, baseline);
ctx.textAlign = 'left';
x = x - this.size.width / 2; // Shift label 1/2-distance to the left
if ((this.fontOptions.valign) && (this.size.height > this.size.labelHeight)) {
if (this.fontOptions.valign === 'top') {
y -= (this.size.height - this.size.labelHeight) / 2;
}
if (this.fontOptions.valign === 'bottom') {
y += (this.size.height - this.size.labelHeight) / 2;
}
}
// draw the text
for (let i = 0; i < this.lineCount; i++) {
let line = this.lines[i];
if (line && line.blocks) {
let width = 0;
if (this.isEdgeLabel || this.fontOptions.align === 'center') {
width += (this.size.width - line.width) / 2
} else if (this.fontOptions.align === 'right') {
width += (this.size.width - line.width)
}
for (let j = 0; j < line.blocks.length; j++) {
let block = line.blocks[j];
ctx.font = block.font;
let [fontColor, strokeColor] = this._getColor(block.color, viewFontSize, block.strokeColor);
if (block.strokeWidth > 0) {
ctx.lineWidth = block.strokeWidth;
ctx.strokeStyle = strokeColor;
ctx.lineJoin = 'round';
}
ctx.fillStyle = fontColor;
if (block.strokeWidth > 0) {
ctx.strokeText(block.text, x + width, y + block.vadjust);
}
ctx.fillText(block.text, x + width, y + block.vadjust);
width += block.width;
}
y += line.height;
}
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {number} x
* @param {number} y
* @param {string} baseline
* @returns {Array.<number>}
* @private
*/
_setAlignment(ctx, x, y, baseline) {
// check for label alignment (for edges)
// TODO: make alignment for nodes
if (this.isEdgeLabel && this.fontOptions.align !== 'horizontal' && this.pointToSelf === false) {
x = 0;
y = 0;
let lineMargin = 2;
if (this.fontOptions.align === 'top') {
ctx.textBaseline = 'alphabetic';
y -= 2 * lineMargin; // distance from edge, required because we use alphabetic. Alphabetic has less difference between browsers
}
else if (this.fontOptions.align === 'bottom') {
ctx.textBaseline = 'hanging';
y += 2 * lineMargin;// distance from edge, required because we use hanging. Hanging has less difference between browsers
}
else {
ctx.textBaseline = 'middle';
}
}
else {
ctx.textBaseline = baseline;
}
return [x,y];
}
/**
* fade in when relative scale is between threshold and threshold - 1.
* If the relative scale would be smaller than threshold -1 the draw function would have returned before coming here.
*
* @param {string} color The font color to use
* @param {number} viewFontSize
* @param {string} initialStrokeColor
* @returns {Array.<string>} An array containing the font color and stroke color
* @private
*/
_getColor(color, viewFontSize, initialStrokeColor) {
let fontColor = color || '#000000';
let strokeColor = initialStrokeColor || '#ffffff';
if (viewFontSize <= this.elementOptions.scaling.label.drawThreshold) {
let opacity = Math.max(0, Math.min(1, 1 - (this.elementOptions.scaling.label.drawThreshold - viewFontSize)));
fontColor = util.overrideOpacity(fontColor, opacity);
strokeColor = util.overrideOpacity(strokeColor, opacity);
}
return [fontColor, strokeColor];
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @returns {{width: number, height: number}}
*/
getTextSize(ctx, selected = false, hover = false) {
this._processLabel(ctx, selected, hover);
return {
width: this.size.width,
height: this.size.height,
lineCount: this.lineCount
};
}
/**
* Get the current dimensions of the label
*
* @return {rect}
*/
getSize() {
let lineMargin = 2;
let x = this.size.left; // default values which might be overridden below
let y = this.size.top - 0.5*lineMargin; // idem
if (this.isEdgeLabel) {
const x2 = -this.size.width * 0.5;
switch (this.fontOptions.align) {
case 'middle':
x = x2;
y = -this.size.height * 0.5
break;
case 'top':
x = x2;
y = -(this.size.height + lineMargin);
break;
case 'bottom':
x = x2;
y = lineMargin;
break;
}
}
var ret = {
left : x,
top : y,
width : this.size.width,
height: this.size.height,
};
return ret;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @param {number} [x=0]
* @param {number} [y=0]
* @param {'middle'|'hanging'} [baseline='middle']
*/
calculateLabelSize(ctx, selected, hover, x = 0, y = 0, baseline = 'middle') {
this._processLabel(ctx, selected, hover);
this.size.left = x - this.size.width * 0.5;
this.size.top = y - this.size.height * 0.5;
this.size.yLine = y + (1 - this.lineCount) * 0.5 * this.fontOptions.size;
if (baseline === "hanging") {
this.size.top += 0.5 * this.fontOptions.size;
this.size.top += 4; // distance from node, required because we use hanging. Hanging has less difference between browsers
this.size.yLine += 4; // distance from node
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @param {string} mod
* @returns {{color, size, face, mod, vadjust, strokeWidth: *, strokeColor: (*|string|allOptions.edges.font.strokeColor|{string}|allOptions.nodes.font.strokeColor|Array)}}
*/
getFormattingValues(ctx, selected, hover, mod) {
let getValue = function(fontOptions, mod, option) {
if (mod === "normal") {
if (option === 'mod' ) return "";
return fontOptions[option];
}
if (fontOptions[mod][option] !== undefined) { // Grumbl leaving out test on undefined equals false for ""
return fontOptions[mod][option];
} else {
// Take from parent font option
return fontOptions[option];
}
};
let values = {
color : getValue(this.fontOptions, mod, 'color' ),
size : getValue(this.fontOptions, mod, 'size' ),
face : getValue(this.fontOptions, mod, 'face' ),
mod : getValue(this.fontOptions, mod, 'mod' ),
vadjust: getValue(this.fontOptions, mod, 'vadjust'),
strokeWidth: this.fontOptions.strokeWidth,
strokeColor: this.fontOptions.strokeColor
};
if (selected || hover) {
if (mod === "normal" && (this.fontOptions.chooser === true) && (this.elementOptions.labelHighlightBold)) {
values.mod = 'bold';
} else {
if (typeof this.fontOptions.chooser === 'function') {
this.fontOptions.chooser(values, this.elementOptions.id, selected, hover);
}
}
}
let fontString = "";
if (values.mod !== undefined && values.mod !== "") { // safeguard for undefined - this happened
fontString += values.mod + " ";
}
fontString += values.size + "px " + values.face;
ctx.font = fontString.replace(/"/g, "");
values.font = ctx.font;
values.height = values.size;
return values;
}
/**
*
* @param {boolean} selected
* @param {boolean} hover
* @returns {boolean}
*/
differentState(selected, hover) {
return ((selected !== this.selectedState) || (hover !== this.hoverState));
}
/**
* This explodes the passed text into lines and determines the width, height and number of lines.
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @param {string} inText the text to explode
* @returns {{width, height, lines}|*}
* @private
*/
_processLabelText(ctx, selected, hover, inText) {
let splitter = new LabelSplitter(ctx, this, selected, hover);
return splitter.process(inText);
}
/**
* This explodes the label string into lines and sets the width, height and number of lines.
* @param {CanvasRenderingContext2D} ctx
* @param {boolean} selected
* @param {boolean} hover
* @private
*/
_processLabel(ctx, selected, hover) {
if(this.labelDirty === false && !this.differentState(selected,hover))
return;
let state = this._processLabelText(ctx, selected, hover, this.elementOptions.label);
if ((this.fontOptions.minWdt > 0) && (state.width < this.fontOptions.minWdt)) {
state.width = this.fontOptions.minWdt;
}
this.size.labelHeight =state.height;
if ((this.fontOptions.minHgt > 0) && (state.height < this.fontOptions.minHgt)) {
state.height = this.fontOptions.minHgt;
}
this.lines = state.lines;
this.lineCount = state.lines.length;
this.size.width = state.width;
this.size.height = state.height;
this.selectedState = selected;
this.hoverState = hover;
this.labelDirty = false;
}
/**
* Check if this label is visible
*
* @return {boolean} true if this label will be show, false otherwise
*/
visible() {
if ((this.size.width === 0 || this.size.height === 0)
|| this.elementOptions.label === undefined) {
return false; // nothing to display
}
let viewFontSize = this.fontOptions.size * this.body.view.scale;
if (viewFontSize < this.elementOptions.scaling.label.drawThreshold - 1) {
return false; // Too small or too far away to show
}
return true;
}
}
export default Label;

View File

@@ -0,0 +1,238 @@
/**
* Callback to determine text dimensions, using the parent label settings.
* @callback MeasureText
* @param {text} text
* @param {text} mod
* @return {Object} { width, values} width in pixels and font attributes
*/
/**
* Helper class for Label which collects results of splitting labels into lines and blocks.
*
* @private
*/
class LabelAccumulator {
/**
* @param {MeasureText} measureText
*/
constructor(measureText) {
this.measureText = measureText;
this.current = 0;
this.width = 0;
this.height = 0;
this.lines = [];
}
/**
* Append given text to the given line.
*
* @param {number} l index of line to add to
* @param {string} text string to append to line
* @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
* @private
*/
_add(l, text, mod = 'normal') {
if (this.lines[l] === undefined) {
this.lines[l] = {
width : 0,
height: 0,
blocks: []
};
}
// We still need to set a block for undefined and empty texts, hence return at this point
// This is necessary because we don't know at this point if we're at the
// start of an empty line or not.
// To compensate, empty blocks are removed in `finalize()`.
//
// Empty strings should still have a height
let tmpText = text;
if (text === undefined || text === "") tmpText = " ";
// Determine width and get the font properties
let result = this.measureText(tmpText, mod);
let block = Object.assign({}, result.values);
block.text = text;
block.width = result.width;
block.mod = mod;
if (text === undefined || text === "") {
block.width = 0;
}
this.lines[l].blocks.push(block);
// Update the line width. We need this for determining if a string goes over max width
this.lines[l].width += block.width;
}
/**
* Returns the width in pixels of the current line.
*
* @returns {number}
*/
curWidth() {
let line = this.lines[this.current];
if (line === undefined) return 0;
return line.width;
}
/**
* Add text in block to current line
*
* @param {string} text
* @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
*/
append(text, mod = 'normal') {
this._add(this.current, text, mod);
}
/**
* Add text in block to current line and start a new line
*
* @param {string} text
* @param {'bold'|'ital'|'boldital'|'mono'|'normal'} [mod='normal']
*/
newLine(text, mod = 'normal') {
this._add(this.current, text, mod);
this.current++;
}
/**
* Determine and set the heights of all the lines currently contained in this instance
*
* Note that width has already been set.
*
* @private
*/
determineLineHeights() {
for (let k = 0; k < this.lines.length; k++) {
let line = this.lines[k];
// Looking for max height of blocks in line
let height = 0;
if (line.blocks !== undefined) { // Can happen if text contains e.g. '\n '
for (let l = 0; l < line.blocks.length; l++) {
let block = line.blocks[l];
if (height < block.height) {
height = block.height;
}
}
}
line.height = height;
}
}
/**
* Determine the full size of the label text, as determined by current lines and blocks
*
* @private
*/
determineLabelSize() {
let width = 0;
let height = 0;
for (let k = 0; k < this.lines.length; k++) {
let line = this.lines[k];
if (line.width > width) {
width = line.width;
}
height += line.height;
}
this.width = width;
this.height = height;
}
/**
* Remove all empty blocks and empty lines we don't need
*
* This must be done after the width/height determination,
* so that these are set properly for processing here.
*
* @returns {Array<Line>} Lines with empty blocks (and some empty lines) removed
* @private
*/
removeEmptyBlocks() {
let tmpLines = [];
for (let k = 0; k < this.lines.length; k++) {
let line = this.lines[k];
// Note: an empty line in between text has width zero but is still relevant to layout.
// So we can't use width for testing empty line here
if (line.blocks.length === 0) continue;
// Discard final empty line always
if(k === this.lines.length - 1) {
if (line.width === 0) continue;
}
let tmpLine = {};
Object.assign(tmpLine, line);
tmpLine.blocks = [];
let firstEmptyBlock;
let tmpBlocks = []
for (let l = 0; l < line.blocks.length; l++) {
let block = line.blocks[l];
if (block.width !== 0) {
tmpBlocks.push(block);
} else {
if (firstEmptyBlock === undefined) {
firstEmptyBlock = block;
}
}
}
// Ensure that there is *some* text present
if (tmpBlocks.length === 0 && firstEmptyBlock !== undefined) {
tmpBlocks.push(firstEmptyBlock);
}
tmpLine.blocks = tmpBlocks;
tmpLines.push(tmpLine);
}
return tmpLines;
}
/**
* Set the sizes for all lines and the whole thing.
*
* @returns {{width: (number|*), height: (number|*), lines: Array}}
*/
finalize() {
//console.log(JSON.stringify(this.lines, null, 2));
this.determineLineHeights();
this.determineLabelSize();
let tmpLines = this.removeEmptyBlocks();
// Return a simple hash object for further processing.
return {
width : this.width,
height: this.height,
lines : tmpLines
}
}
}
export default LabelAccumulator;

View File

@@ -0,0 +1,549 @@
let LabelAccumulator = require('./LabelAccumulator').default;
let ComponentUtil = require('./ComponentUtil').default;
/**
* Helper class for Label which explodes the label text into lines and blocks within lines
*
* @private
*/
class LabelSplitter {
/**
* @param {CanvasRenderingContext2D} ctx Canvas rendering context
* @param {Label} parent reference to the Label instance using current instance
* @param {boolean} selected
* @param {boolean} hover
*/
constructor(ctx, parent, selected, hover) {
this.ctx = ctx;
this.parent = parent;
/**
* Callback to determine text width; passed to LabelAccumulator instance
*
* @param {String} text string to determine width of
* @param {String} mod font type to use for this text
* @return {Object} { width, values} width in pixels and font attributes
*/
let textWidth = (text, mod) => {
if (text === undefined) return 0;
// TODO: This can be done more efficiently with caching
let values = this.parent.getFormattingValues(ctx, selected, hover, mod);
let width = 0;
if (text !== '') {
// NOTE: The following may actually be *incorrect* for the mod fonts!
// This returns the size with a regular font, bold etc. may
// have different sizes.
let measure = this.ctx.measureText(text);
width = measure.width;
}
return {width, values: values};
};
this.lines = new LabelAccumulator(textWidth);
}
/**
* Split passed text of a label into lines and blocks.
*
* # NOTE
*
* The handling of spacing is option dependent:
*
* - if `font.multi : false`, all spaces are retained
* - if `font.multi : true`, every sequence of spaces is compressed to a single space
*
* This might not be the best way to do it, but this is as it has been working till now.
* In order not to break existing functionality, for the time being this behaviour will
* be retained in any code changes.
*
* @param {string} text text to split
* @returns {Array<line>}
*/
process(text) {
if (!ComponentUtil.isValidLabel(text)) {
return this.lines.finalize();
}
var font = this.parent.fontOptions;
// Normalize the end-of-line's to a single representation - order important
text = text.replace(/\r\n/g, '\n'); // Dos EOL's
text = text.replace(/\r/g, '\n'); // Mac EOL's
// Note that at this point, there can be no \r's in the text.
// This is used later on splitStringIntoLines() to split multifont texts.
let nlLines = String(text).split('\n');
let lineCount = nlLines.length;
if (font.multi) {
// Multi-font case: styling tags active
for (let i = 0; i < lineCount; i++) {
let blocks = this.splitBlocks(nlLines[i], font.multi);
// Post: Sequences of tabs and spaces are reduced to single space
if (blocks === undefined) continue;
if (blocks.length === 0) {
this.lines.newLine("");
continue;
}
if (font.maxWdt > 0) {
// widthConstraint.maximum defined
//console.log('Running widthConstraint multi, max: ' + this.fontOptions.maxWdt);
for (let j = 0; j < blocks.length; j++) {
let mod = blocks[j].mod;
let text = blocks[j].text;
this.splitStringIntoLines(text, mod, true);
}
} else {
// widthConstraint.maximum NOT defined
for (let j = 0; j < blocks.length; j++) {
let mod = blocks[j].mod;
let text = blocks[j].text;
this.lines.append(text, mod);
}
}
this.lines.newLine();
}
} else {
// Single-font case
if (font.maxWdt > 0) {
// widthConstraint.maximum defined
// console.log('Running widthConstraint normal, max: ' + this.fontOptions.maxWdt);
for (let i = 0; i < lineCount; i++) {
this.splitStringIntoLines(nlLines[i]);
}
} else {
// widthConstraint.maximum NOT defined
for (let i = 0; i < lineCount; i++) {
this.lines.newLine(nlLines[i]);
}
}
}
return this.lines.finalize();
}
/**
* normalize the markup system
*
* @param {boolean|'md'|'markdown'|'html'} markupSystem
* @returns {string}
*/
decodeMarkupSystem(markupSystem) {
let system = 'none';
if (markupSystem === 'markdown' || markupSystem === 'md') {
system = 'markdown';
} else if (markupSystem === true || markupSystem === 'html') {
system = 'html'
}
return system;
}
/**
*
* @param {string} text
* @returns {Array}
*/
splitHtmlBlocks(text) {
let blocks = [];
// TODO: consolidate following + methods/closures with splitMarkdownBlocks()
// NOTE: sequences of tabs and spaces are reduced to single space; scan usage of `this.spacing` within method
let s = {
bold: false,
ital: false,
mono: false,
spacing: false,
position: 0,
buffer: "",
modStack: []
};
s.mod = function() {
return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
};
s.modName = function() {
if (this.modStack.length === 0)
return 'normal';
else if (this.modStack[0] === 'mono')
return 'mono';
else {
if (s.bold && s.ital) {
return 'boldital';
} else if (s.bold) {
return 'bold';
} else if (s.ital) {
return 'ital';
}
}
};
s.emitBlock = function(override=false) { // eslint-disable-line no-unused-vars
if (this.spacing) {
this.add(" ");
this.spacing = false;
}
if (this.buffer.length > 0) {
blocks.push({ text: this.buffer, mod: this.modName() });
this.buffer = "";
}
};
s.add = function(text) {
if (text === " ") {
s.spacing = true;
}
if (s.spacing) {
this.buffer += " ";
this.spacing = false;
}
if (text != " ") {
this.buffer += text;
}
};
while (s.position < text.length) {
let ch = text.charAt(s.position);
if (/[ \t]/.test(ch)) {
if (!s.mono) {
s.spacing = true;
} else {
s.add(ch);
}
} else if (/</.test(ch)) {
if (!s.mono && !s.bold && /<b>/.test(text.substr(s.position,3))) {
s.emitBlock();
s.bold = true;
s.modStack.unshift("bold");
s.position += 2;
} else if (!s.mono && !s.ital && /<i>/.test(text.substr(s.position,3))) {
s.emitBlock();
s.ital = true;
s.modStack.unshift("ital");
s.position += 2;
} else if (!s.mono && /<code>/.test(text.substr(s.position,6))) {
s.emitBlock();
s.mono = true;
s.modStack.unshift("mono");
s.position += 5;
} else if (!s.mono && (s.mod() === 'bold') && /<\/b>/.test(text.substr(s.position,4))) {
s.emitBlock();
s.bold = false;
s.modStack.shift();
s.position += 3;
} else if (!s.mono && (s.mod() === 'ital') && /<\/i>/.test(text.substr(s.position,4))) {
s.emitBlock();
s.ital = false;
s.modStack.shift();
s.position += 3;
} else if ((s.mod() === 'mono') && /<\/code>/.test(text.substr(s.position,7))) {
s.emitBlock();
s.mono = false;
s.modStack.shift();
s.position += 6;
} else {
s.add(ch);
}
} else if (/&/.test(ch)) {
if (/&lt;/.test(text.substr(s.position,4))) {
s.add("<");
s.position += 3;
} else if (/&amp;/.test(text.substr(s.position,5))) {
s.add("&");
s.position += 4;
} else {
s.add("&");
}
} else {
s.add(ch);
}
s.position++
}
s.emitBlock();
return blocks;
}
/**
*
* @param {string} text
* @returns {Array}
*/
splitMarkdownBlocks(text) {
let blocks = [];
// TODO: consolidate following + methods/closures with splitHtmlBlocks()
// NOTE: sequences of tabs and spaces are reduced to single space; scan usage of `this.spacing` within method
let s = {
bold: false,
ital: false,
mono: false,
beginable: true,
spacing: false,
position: 0,
buffer: "",
modStack: []
};
s.mod = function() {
return (this.modStack.length === 0) ? 'normal' : this.modStack[0];
};
s.modName = function() {
if (this.modStack.length === 0)
return 'normal';
else if (this.modStack[0] === 'mono')
return 'mono';
else {
if (s.bold && s.ital) {
return 'boldital';
} else if (s.bold) {
return 'bold';
} else if (s.ital) {
return 'ital';
}
}
};
s.emitBlock = function(override=false) { // eslint-disable-line no-unused-vars
if (this.spacing) {
this.add(" ");
this.spacing = false;
}
if (this.buffer.length > 0) {
blocks.push({ text: this.buffer, mod: this.modName() });
this.buffer = "";
}
};
s.add = function(text) {
if (text === " ") {
s.spacing = true;
}
if (s.spacing) {
this.buffer += " ";
this.spacing = false;
}
if (text != " ") {
this.buffer += text;
}
};
while (s.position < text.length) {
let ch = text.charAt(s.position);
if (/[ \t]/.test(ch)) {
if (!s.mono) {
s.spacing = true;
} else {
s.add(ch);
}
s.beginable = true
} else if (/\\/.test(ch)) {
if (s.position < text.length+1) {
s.position++;
ch = text.charAt(s.position);
if (/ \t/.test(ch)) {
s.spacing = true;
} else {
s.add(ch);
s.beginable = false;
}
}
} else if (!s.mono && !s.bold && (s.beginable || s.spacing) && /\*/.test(ch)) {
s.emitBlock();
s.bold = true;
s.modStack.unshift("bold");
} else if (!s.mono && !s.ital && (s.beginable || s.spacing) && /\_/.test(ch)) {
s.emitBlock();
s.ital = true;
s.modStack.unshift("ital");
} else if (!s.mono && (s.beginable || s.spacing) && /`/.test(ch)) {
s.emitBlock();
s.mono = true;
s.modStack.unshift("mono");
} else if (!s.mono && (s.mod() === "bold") && /\*/.test(ch)) {
if ((s.position === text.length-1) || /[.,_` \t\n]/.test(text.charAt(s.position+1))) {
s.emitBlock();
s.bold = false;
s.modStack.shift();
} else {
s.add(ch);
}
} else if (!s.mono && (s.mod() === "ital") && /\_/.test(ch)) {
if ((s.position === text.length-1) || /[.,*` \t\n]/.test(text.charAt(s.position+1))) {
s.emitBlock();
s.ital = false;
s.modStack.shift();
} else {
s.add(ch);
}
} else if (s.mono && (s.mod() === "mono") && /`/.test(ch)) {
if ((s.position === text.length-1) || (/[.,*_ \t\n]/.test(text.charAt(s.position+1)))) {
s.emitBlock();
s.mono = false;
s.modStack.shift();
} else {
s.add(ch);
}
} else {
s.add(ch);
s.beginable = false;
}
s.position++
}
s.emitBlock();
return blocks;
}
/**
* Explodes a piece of text into single-font blocks using a given markup
*
* @param {string} text
* @param {boolean|'md'|'markdown'|'html'} markupSystem
* @returns {Array.<{text: string, mod: string}>}
* @private
*/
splitBlocks(text, markupSystem) {
let system = this.decodeMarkupSystem(markupSystem);
if (system === 'none') {
return [{
text: text,
mod: 'normal'
}]
} else if (system === 'markdown') {
return this.splitMarkdownBlocks(text);
} else if (system === 'html') {
return this.splitHtmlBlocks(text);
}
}
/**
* @param {string} text
* @returns {boolean} true if text length over the current max with
* @private
*/
overMaxWidth(text) {
let width = this.ctx.measureText(text).width;
return (this.lines.curWidth() + width > this.parent.fontOptions.maxWdt);
}
/**
* Determine the longest part of the sentence which still fits in the
* current max width.
*
* @param {Array} words Array of strings signifying a text lines
* @return {number} index of first item in string making string go over max
* @private
*/
getLongestFit(words) {
let text = '';
let w = 0;
while (w < words.length) {
let pre = (text === '') ? '' : ' ';
let newText = text + pre + words[w];
if (this.overMaxWidth(newText)) break;
text = newText;
w++;
}
return w;
}
/**
* Determine the longest part of the string which still fits in the
* current max width.
*
* @param {Array} words Array of strings signifying a text lines
* @return {number} index of first item in string making string go over max
*/
getLongestFitWord(words) {
let w = 0;
while (w < words.length) {
if (this.overMaxWidth(words.slice(0,w))) break;
w++;
}
return w;
}
/**
* Split the passed text into lines, according to width constraint (if any).
*
* The method assumes that the input string is a single line, i.e. without lines break.
*
* This method retains spaces, if still present (case `font.multi: false`).
* A space which falls on an internal line break, will be replaced by a newline.
* There is no special handling of tabs; these go along with the flow.
*
* @param {string} str
* @param {string} [mod='normal']
* @param {boolean} [appendLast=false]
* @private
*/
splitStringIntoLines(str, mod = 'normal', appendLast = false) {
// Still-present spaces are relevant, retain them
str = str.replace(/^( +)/g, '$1\r');
str = str.replace(/([^\r][^ ]*)( +)/g, '$1\r$2\r');
let words = str.split('\r');
while (words.length > 0) {
let w = this.getLongestFit(words);
if (w === 0) {
// Special case: the first word is already larger than the max width.
let word = words[0];
// Break the word to the largest part that fits the line
let x = this.getLongestFitWord(word);
this.lines.newLine(word.slice(0, x), mod);
// Adjust the word, so that the rest will be done next iteration
words[0] = word.slice(x);
} else {
// skip any space that is replaced by a newline
let newW = w;
if (words[w - 1] === ' ') {
w--;
} else if (words[newW] === ' ') {
newW++;
}
let text = words.slice(0, w).join("");
if (w == words.length && appendLast) {
this.lines.append(text, mod);
} else {
this.lines.newLine(text, mod);
}
// Adjust the word, so that the rest will be done next iteration
words = words.slice(newW);
}
}
}
}
export default LabelSplitter;