visjs-network
Version:
A dynamic, browser-based network visualization library.
688 lines (628 loc) • 19.3 kB
JavaScript
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)
}
}
// prettier-ignore
/**
*
* @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)
}
}
}
// prettier-ignore
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {ArrowOptions} 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)
}
// prettier-ignore
/**
* 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 {ArrowOptions} 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 {ArrowOptions} 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 {ArrowOptions} values
*/
disableShadow(ctx, values) {
if (values.shadow === true) {
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
*/
drawBackground(ctx, values) {
if (values.background !== false) {
let attrs = ['strokeStyle', 'lineWidth', 'dashes']
let origCtxAttr = {}
// save original line attrs
attrs.forEach(function(attrname) {
origCtxAttr[attrname] = ctx[attrname]
})
ctx.strokeStyle = values.backgroundColor
ctx.lineWidth = values.backgroundSize
this.setStrokeDashed(ctx, values.backgroundDashes)
ctx.stroke()
// restore original line attrs
attrs.forEach(function(attrname) {
ctx[attrname] = origCtxAttr[attrname]
})
this.setStrokeDashed(ctx, values.dashes)
}
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {boolean|Array} dashes
*/
setStrokeDashed(ctx, dashes) {
if (dashes !== false) {
if (ctx.setLineDash !== undefined) {
let pattern = [5, 5]
if (Array.isArray(dashes) === true) {
pattern = dashes
}
ctx.setLineDash(pattern)
} else {
console.warn(
'setLineDash is not supported in this browser. The dashed stroke cannot be used.'
)
}
} else {
if (ctx.setLineDash !== undefined) {
ctx.setLineDash([])
} else {
console.warn(
'setLineDash is not supported in this browser. The dashed stroke cannot be used.'
)
}
}
}
}
export default EdgeBase