mirror of
https://github.com/datahub-project/datahub.git
synced 2025-10-12 01:18:11 +00:00
829 lines
21 KiB
JavaScript
829 lines
21 KiB
JavaScript
var layout = require('dagre').layout;
|
|
|
|
var d3;
|
|
try { d3 = require('d3'); } catch (_) { d3 = window.d3; }
|
|
|
|
module.exports = Renderer;
|
|
|
|
function Renderer() {
|
|
// Set up defaults...
|
|
this._layout = layout();
|
|
|
|
this.drawNodes(defaultDrawNodes);
|
|
this.drawEdgeLabels(defaultDrawEdgeLabels);
|
|
this.drawEdgePaths(defaultDrawEdgePaths);
|
|
this.positionNodes(defaultPositionNodes);
|
|
this.positionEdgeLabels(defaultPositionEdgeLabels);
|
|
this.positionEdgePaths(defaultPositionEdgePaths);
|
|
this.zoomSetup(defaultZoomSetup);
|
|
this.zoom(defaultZoom);
|
|
this.transition(defaultTransition);
|
|
this.postLayout(defaultPostLayout);
|
|
this.postRender(defaultPostRender);
|
|
|
|
this.edgeInterpolate('bundle');
|
|
this.edgeTension(0.95);
|
|
}
|
|
|
|
Renderer.prototype.layout = function(layout) {
|
|
if (!arguments.length) { return this._layout; }
|
|
this._layout = layout;
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.drawNodes = function(drawNodes) {
|
|
if (!arguments.length) { return this._drawNodes; }
|
|
this._drawNodes = bind(drawNodes, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.drawEdgeLabels = function(drawEdgeLabels) {
|
|
if (!arguments.length) { return this._drawEdgeLabels; }
|
|
this._drawEdgeLabels = bind(drawEdgeLabels, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.drawEdgePaths = function(drawEdgePaths) {
|
|
if (!arguments.length) { return this._drawEdgePaths; }
|
|
this._drawEdgePaths = bind(drawEdgePaths, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.positionNodes = function(positionNodes) {
|
|
if (!arguments.length) { return this._positionNodes; }
|
|
this._positionNodes = bind(positionNodes, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.positionEdgeLabels = function(positionEdgeLabels) {
|
|
if (!arguments.length) { return this._positionEdgeLabels; }
|
|
this._positionEdgeLabels = bind(positionEdgeLabels, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.positionEdgePaths = function(positionEdgePaths) {
|
|
if (!arguments.length) { return this._positionEdgePaths; }
|
|
this._positionEdgePaths = bind(positionEdgePaths, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.transition = function(transition) {
|
|
if (!arguments.length) { return this._transition; }
|
|
this._transition = bind(transition, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.zoomSetup = function(zoomSetup) {
|
|
if (!arguments.length) { return this._zoomSetup; }
|
|
this._zoomSetup = bind(zoomSetup, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.zoom = function(zoom) {
|
|
if (!arguments.length) { return this._zoom; }
|
|
if (zoom) {
|
|
this._zoom = bind(zoom, this);
|
|
} else {
|
|
delete this._zoom;
|
|
}
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.postLayout = function(postLayout) {
|
|
if (!arguments.length) { return this._postLayout; }
|
|
this._postLayout = bind(postLayout, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.postRender = function(postRender) {
|
|
if (!arguments.length) { return this._postRender; }
|
|
this._postRender = bind(postRender, this);
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.edgeInterpolate = function(edgeInterpolate) {
|
|
if (!arguments.length) { return this._edgeInterpolate; }
|
|
this._edgeInterpolate = edgeInterpolate;
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.edgeTension = function(edgeTension) {
|
|
if (!arguments.length) { return this._edgeTension; }
|
|
this._edgeTension = edgeTension;
|
|
return this;
|
|
};
|
|
|
|
Renderer.prototype.run = function(graph, orgSvg) {
|
|
// First copy the input graph so that it is not changed by the rendering
|
|
// process.
|
|
graph = copyAndInitGraph(graph);
|
|
|
|
// Create zoom elements
|
|
var svg = this._zoomSetup(graph, orgSvg);
|
|
|
|
// Create layers
|
|
svg
|
|
.selectAll('g.edgePaths, g.edgeLabels, g.nodes')
|
|
.data(['edgePaths', 'edgeLabels', 'nodes'])
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', function(d) { return d; });
|
|
|
|
// Create node and edge roots, attach labels, and capture dimension
|
|
// information for use with layout.
|
|
var svgNodes = this._drawNodes(graph, svg.select('g.nodes'));
|
|
var svgEdgeLabels = this._drawEdgeLabels(graph, svg.select('g.edgeLabels'));
|
|
|
|
svgNodes.each(function(u) { calculateDimensions(this, graph.node(u)); });
|
|
svgEdgeLabels.each(function(e) { calculateDimensions(this, graph.edge(e)); });
|
|
|
|
// Now apply the layout function
|
|
var result = runLayout(graph, this._layout);
|
|
|
|
// Copy useDef attribute from input graph to output graph
|
|
graph.eachNode(function(u, a) {
|
|
if (a.useDef) {
|
|
result.node(u).useDef = a.useDef;
|
|
}
|
|
});
|
|
|
|
// Run any user-specified post layout processing
|
|
this._postLayout(result, svg);
|
|
|
|
var svgEdgePaths = this._drawEdgePaths(graph, svg.select('g.edgePaths'));
|
|
|
|
// Apply the layout information to the graph
|
|
this._positionNodes(result, svgNodes);
|
|
this._positionEdgeLabels(result, svgEdgeLabels);
|
|
this._positionEdgePaths(result, svgEdgePaths, orgSvg);
|
|
|
|
this._postRender(result, svg);
|
|
|
|
return result;
|
|
};
|
|
|
|
function copyAndInitGraph(graph) {
|
|
var copy = graph.copy();
|
|
|
|
if (copy.graph() === undefined) {
|
|
copy.graph({});
|
|
}
|
|
|
|
if (!('arrowheadFix' in copy.graph())) {
|
|
copy.graph().arrowheadFix = true;
|
|
}
|
|
|
|
// Init labels if they were not present in the source graph
|
|
copy.nodes().forEach(function(u) {
|
|
var value = copyObject(copy.node(u));
|
|
copy.node(u, value);
|
|
if (!('label' in value)) { value.label = ''; }
|
|
});
|
|
|
|
copy.edges().forEach(function(e) {
|
|
var value = copyObject(copy.edge(e));
|
|
copy.edge(e, value);
|
|
if (!('label' in value)) { value.label = ''; }
|
|
});
|
|
|
|
return copy;
|
|
}
|
|
|
|
function copyObject(obj) {
|
|
var copy = {};
|
|
for (var k in obj) {
|
|
copy[k] = obj[k];
|
|
}
|
|
return copy;
|
|
}
|
|
|
|
function calculateDimensions(group, value) {
|
|
var bbox = group.getBBox();
|
|
value.width = bbox.width;
|
|
value.height = bbox.height;
|
|
}
|
|
|
|
function runLayout(graph, layout) {
|
|
var result = layout.run(graph);
|
|
|
|
// Copy labels to the result graph
|
|
graph.eachNode(function(u, value) { result.node(u).label = value.label; });
|
|
graph.eachEdge(function(e, u, v, value) { result.edge(e).label = value.label; });
|
|
|
|
return result;
|
|
}
|
|
|
|
function defaultDrawNodes(g, root) {
|
|
var nodes = g.nodes().filter(function(u) { return !isComposite(g, u); });
|
|
|
|
var svgNodes = root
|
|
.selectAll('g.node')
|
|
.classed('enter', false)
|
|
.data(nodes, function(u) { return u; });
|
|
|
|
svgNodes.selectAll('*').remove();
|
|
|
|
svgNodes
|
|
.enter()
|
|
.append('g')
|
|
.style('opacity', 0)
|
|
.attr('class', 'node enter');
|
|
|
|
svgNodes.each(function(u) {
|
|
var attrs = g.node(u),
|
|
domNode = d3.select(this);
|
|
addLabel(attrs, domNode, true, 10, 10);
|
|
});
|
|
|
|
this._transition(svgNodes.exit())
|
|
.style('opacity', 0)
|
|
.remove();
|
|
|
|
return svgNodes;
|
|
}
|
|
|
|
function defaultDrawEdgeLabels(g, root) {
|
|
var svgEdgeLabels = root
|
|
.selectAll('g.edgeLabel')
|
|
.classed('enter', false)
|
|
.data(g.edges(), function (e) { return e; });
|
|
|
|
svgEdgeLabels.selectAll('*').remove();
|
|
|
|
svgEdgeLabels
|
|
.enter()
|
|
.append('g')
|
|
.style('opacity', 0)
|
|
.attr('class', 'edgeLabel enter');
|
|
|
|
svgEdgeLabels.each(function(e) { addLabel(g.edge(e), d3.select(this), false, 0, 0); });
|
|
|
|
this._transition(svgEdgeLabels.exit())
|
|
.style('opacity', 0)
|
|
.remove();
|
|
|
|
return svgEdgeLabels;
|
|
}
|
|
|
|
var defaultDrawEdgePaths = function(g, root) {
|
|
var svgEdgePaths = root
|
|
.selectAll('g.edgePath')
|
|
.classed('enter', false)
|
|
.data(g.edges(), function(e) { return e; });
|
|
|
|
var DEFAULT_ARROWHEAD = 'url(#arrowhead)',
|
|
createArrowhead = DEFAULT_ARROWHEAD;
|
|
if (!g.isDirected()) {
|
|
createArrowhead = null;
|
|
} else if (g.graph().arrowheadFix !== 'false' && g.graph().arrowheadFix !== false) {
|
|
createArrowhead = function() {
|
|
var strokeColor = d3.select(this).style('stroke');
|
|
if (strokeColor) {
|
|
var id = 'arrowhead-' + strokeColor.replace(/[^a-zA-Z0-9]/g, '_');
|
|
getOrMakeArrowhead(root, id).style('fill', strokeColor);
|
|
return 'url(#' + id + ')';
|
|
}
|
|
return DEFAULT_ARROWHEAD;
|
|
};
|
|
}
|
|
|
|
svgEdgePaths
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', 'edgePath enter')
|
|
.append('path')
|
|
.style('opacity', 0);
|
|
|
|
svgEdgePaths
|
|
.selectAll('path')
|
|
.each(function(e) { applyStyle(g.edge(e).style, d3.select(this)); })
|
|
.attr('marker-end', createArrowhead);
|
|
|
|
this._transition(svgEdgePaths.exit())
|
|
.style('opacity', 0)
|
|
.remove();
|
|
|
|
return svgEdgePaths;
|
|
};
|
|
|
|
function defaultPositionNodes(g, svgNodes) {
|
|
function transform(u) {
|
|
var value = g.node(u);
|
|
return 'translate(' + value.x + ',' + value.y + ')';
|
|
}
|
|
|
|
// For entering nodes, position immediately without transition
|
|
svgNodes.filter('.enter').attr('transform', transform);
|
|
|
|
this._transition(svgNodes)
|
|
.style('opacity', 1)
|
|
.attr('transform', transform);
|
|
}
|
|
|
|
function defaultPositionEdgeLabels(g, svgEdgeLabels) {
|
|
function transform(e) {
|
|
var value = g.edge(e);
|
|
var point = findMidPoint(value.points);
|
|
return 'translate(' + point.x + ',' + point.y + ')';
|
|
}
|
|
|
|
// For entering edge labels, position immediately without transition
|
|
svgEdgeLabels.filter('.enter').attr('transform', transform);
|
|
|
|
this._transition(svgEdgeLabels)
|
|
.style('opacity', 1)
|
|
.attr('transform', transform);
|
|
}
|
|
|
|
function isEllipse(obj) {
|
|
return Object.prototype.toString.call(obj) === '[object SVGEllipseElement]';
|
|
}
|
|
|
|
function isCircle(obj) {
|
|
return Object.prototype.toString.call(obj) === '[object SVGCircleElement]';
|
|
}
|
|
|
|
function isPolygon(obj) {
|
|
return Object.prototype.toString.call(obj) === '[object SVGPolygonElement]';
|
|
}
|
|
|
|
function intersectNode(nd, p1, root) {
|
|
if (nd.useDef) {
|
|
var definedFig = root.select('defs #' + nd.useDef).node();
|
|
if (definedFig) {
|
|
var outerFig = definedFig.childNodes[0];
|
|
if (isCircle(outerFig) || isEllipse(outerFig)) {
|
|
return intersectEllipse(nd, outerFig, p1);
|
|
} else if (isPolygon(outerFig)) {
|
|
return intersectPolygon(nd, outerFig, p1);
|
|
}
|
|
}
|
|
}
|
|
// TODO: use bpodgursky's shortening algorithm here
|
|
return intersectRect(nd, p1);
|
|
}
|
|
|
|
function defaultPositionEdgePaths(g, svgEdgePaths, root) {
|
|
var interpolate = this._edgeInterpolate,
|
|
tension = this._edgeTension;
|
|
|
|
function calcPoints(e) {
|
|
var value = g.edge(e);
|
|
var source = g.node(g.incidentNodes(e)[0]);
|
|
var target = g.node(g.incidentNodes(e)[1]);
|
|
var points = value.points.slice();
|
|
|
|
var p0 = points.length === 0 ? target : points[0];
|
|
var p1 = points.length === 0 ? source : points[points.length - 1];
|
|
|
|
points.unshift(intersectNode(source, p0, root));
|
|
points.push(intersectNode(target, p1, root));
|
|
|
|
return d3.svg.line()
|
|
.x(function(d) { return d.x; })
|
|
.y(function(d) { return d.y; })
|
|
.interpolate(interpolate)
|
|
.tension(tension)
|
|
(points);
|
|
}
|
|
|
|
svgEdgePaths.filter('.enter').selectAll('path')
|
|
.attr('d', calcPoints);
|
|
|
|
this._transition(svgEdgePaths.selectAll('path'))
|
|
.attr('d', calcPoints)
|
|
.style('opacity', 1);
|
|
}
|
|
|
|
// By default we do not use transitions
|
|
function defaultTransition(selection) {
|
|
return selection;
|
|
}
|
|
|
|
// Setup dom for zooming
|
|
function defaultZoomSetup(graph, svg) {
|
|
var root = svg.property('ownerSVGElement');
|
|
// If the svg node is the root, we get null, so set to svg.
|
|
if (!root) {
|
|
root = svg;
|
|
} else {
|
|
root = d3.select(root);
|
|
}
|
|
|
|
if (root.select('rect.overlay').empty()) {
|
|
// Create an overlay for capturing mouse events that don't touch foreground
|
|
root.insert('rect', ':first-child')
|
|
.attr('class', 'overlay')
|
|
.attr('width', '100%')
|
|
.attr('height', '100%')
|
|
.style('fill', 'none')
|
|
.style('pointer-events', 'all');
|
|
|
|
// Capture the zoom behaviour from the svg
|
|
svg = svg.append('g')
|
|
.attr('class', 'zoom');
|
|
|
|
if (this._zoom) {
|
|
root.call(this._zoom(graph, svg));
|
|
}
|
|
}
|
|
|
|
return svg;
|
|
}
|
|
|
|
// By default allow pan and zoom
|
|
function defaultZoom(graph, svg) {
|
|
return d3.behavior.zoom().on('zoom', function() {
|
|
svg.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');
|
|
});
|
|
}
|
|
|
|
function defaultPostLayout() {
|
|
// Do nothing
|
|
}
|
|
|
|
function defaultPostRender(graph, root) {
|
|
if (graph.isDirected()) {
|
|
// Fill = #333 is for backwards compatibility
|
|
getOrMakeArrowhead(root, 'arrowhead')
|
|
.attr('fill', '#333');
|
|
}
|
|
}
|
|
|
|
function getOrMakeArrowhead(root, id) {
|
|
var search = root.select('#' + id);
|
|
if (!search.empty()) { return search; }
|
|
|
|
var defs = root.select('defs');
|
|
if (defs.empty()) {
|
|
defs = root.append('svg:defs');
|
|
}
|
|
|
|
var marker =
|
|
defs
|
|
.append('svg:marker')
|
|
.attr('id', id)
|
|
.attr('viewBox', '0 0 10 10')
|
|
.attr('refX', 8)
|
|
.attr('refY', 5)
|
|
.attr('markerUnits', 'strokeWidth')
|
|
.attr('markerWidth', 8)
|
|
.attr('markerHeight', 5)
|
|
.attr('orient', 'auto');
|
|
|
|
marker
|
|
.append('svg:path')
|
|
.attr('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
|
|
return marker;
|
|
}
|
|
|
|
function addLabel(node, root, addingNode, marginX, marginY) {
|
|
// If the node has 'useDef' meta data, we rely on that
|
|
if (node.useDef) {
|
|
root.append('use').attr('xlink:href', '#' + node.useDef);
|
|
return;
|
|
}
|
|
// Add the rect first so that it appears behind the label
|
|
var label = node.label;
|
|
var rect = root.append('rect');
|
|
if (node.width) {
|
|
rect.attr('width', node.width);
|
|
}
|
|
if (node.height) {
|
|
rect.attr('height', node.height);
|
|
}
|
|
|
|
var labelSvg = root.append('g'),
|
|
innerLabelSvg;
|
|
|
|
if (label[0] === '<') {
|
|
addForeignObjectLabel(label, labelSvg);
|
|
// No margin for HTML elements
|
|
marginX = marginY = 0;
|
|
} else {
|
|
innerLabelSvg = addTextLabel(label,
|
|
labelSvg,
|
|
Math.floor(node.labelCols),
|
|
node.labelCut);
|
|
applyStyle(node.labelStyle, innerLabelSvg);
|
|
}
|
|
|
|
var labelBBox = labelSvg.node().getBBox();
|
|
labelSvg.attr('transform',
|
|
'translate(' + (-labelBBox.width / 2) + ',' + (-labelBBox.height / 2) + ')');
|
|
|
|
var bbox = root.node().getBBox();
|
|
|
|
rect
|
|
.attr('rx', node.rx ? node.rx : 5)
|
|
.attr('ry', node.ry ? node.ry : 5)
|
|
.attr('x', -(bbox.width / 2 + marginX))
|
|
.attr('y', -(bbox.height / 2 + marginY))
|
|
.attr('width', bbox.width + 2 * marginX)
|
|
.attr('height', bbox.height + 2 * marginY)
|
|
.attr('fill', '#fff');
|
|
|
|
if (addingNode) {
|
|
applyStyle(node.style, rect);
|
|
|
|
if (node.fill) {
|
|
rect.style('fill', node.fill);
|
|
}
|
|
|
|
if (node.stroke) {
|
|
rect.style('stroke', node.stroke);
|
|
}
|
|
|
|
if (node['stroke-width']) {
|
|
rect.style('stroke-width', node['stroke-width'] + 'px');
|
|
}
|
|
|
|
if (node['stroke-dasharray']) {
|
|
rect.style('stroke-dasharray', node['stroke-dasharray']);
|
|
}
|
|
|
|
if (node.href) {
|
|
root
|
|
.attr('class', root.attr('class') + ' clickable')
|
|
.on('click', function() {
|
|
window.open(node.href);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function addForeignObjectLabel(label, root) {
|
|
var fo = root
|
|
.append('foreignObject')
|
|
.attr('width', '100000');
|
|
|
|
var w, h;
|
|
fo
|
|
.append('xhtml:div')
|
|
.style('float', 'left')
|
|
// TODO find a better way to get dimensions for foreignObjects...
|
|
.html(function() { return label; })
|
|
.each(function() {
|
|
w = this.clientWidth;
|
|
h = this.clientHeight;
|
|
});
|
|
|
|
fo
|
|
.attr('width', w)
|
|
.attr('height', h);
|
|
}
|
|
|
|
function addTextLabel(label, root, labelCols, labelCut) {
|
|
if (labelCut === undefined) { labelCut = 'false'; }
|
|
labelCut = (labelCut.toString().toLowerCase() === 'true');
|
|
|
|
var node = root
|
|
.append('text')
|
|
.attr('text-anchor', 'left');
|
|
|
|
label = label.replace(/\\n/g, '\n');
|
|
|
|
var arr = labelCols ? wordwrap(label, labelCols, labelCut) : label;
|
|
arr = arr.split('\n');
|
|
for (var i = 0; i < arr.length; i++) {
|
|
node
|
|
.append('tspan')
|
|
.attr('dy', '1em')
|
|
.attr('x', '1')
|
|
.text(arr[i]);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
// Thanks to
|
|
// http://james.padolsey.com/javascript/wordwrap-for-javascript/
|
|
function wordwrap (str, width, cut, brk) {
|
|
brk = brk || '\n';
|
|
width = width || 75;
|
|
cut = cut || false;
|
|
|
|
if (!str) { return str; }
|
|
|
|
var regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\\S+?(\\s|$)');
|
|
|
|
return str.match(new RegExp(regex, 'g')).join(brk);
|
|
}
|
|
|
|
function findMidPoint(points) {
|
|
var midIdx = points.length / 2;
|
|
if (points.length % 2) {
|
|
return points[Math.floor(midIdx)];
|
|
} else {
|
|
var p0 = points[midIdx - 1];
|
|
var p1 = points[midIdx];
|
|
return {x: (p0.x + p1.x) / 2, y: (p0.y + p1.y) / 2};
|
|
}
|
|
}
|
|
|
|
function intersectRect(rect, point) {
|
|
var x = rect.x;
|
|
var y = rect.y;
|
|
|
|
// Rectangle intersection algorithm from:
|
|
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
|
|
var dx = point.x - x;
|
|
var dy = point.y - y;
|
|
var w = rect.width / 2;
|
|
var h = rect.height / 2;
|
|
|
|
var sx, sy;
|
|
if (Math.abs(dy) * w > Math.abs(dx) * h) {
|
|
// Intersection is top or bottom of rect.
|
|
if (dy < 0) {
|
|
h = -h;
|
|
}
|
|
sx = dy === 0 ? 0 : h * dx / dy;
|
|
sy = h;
|
|
} else {
|
|
// Intersection is left or right of rect.
|
|
if (dx < 0) {
|
|
w = -w;
|
|
}
|
|
sx = w;
|
|
sy = dx === 0 ? 0 : w * dy / dx;
|
|
}
|
|
|
|
return {x: x + sx, y: y + sy};
|
|
}
|
|
|
|
function intersectEllipse(node, ellipseOrCircle, point) {
|
|
// Formulae from: http://mathworld.wolfram.com/Ellipse-LineIntersection.html
|
|
|
|
var cx = node.x;
|
|
var cy = node.y;
|
|
var rx, ry;
|
|
|
|
if (isCircle(ellipseOrCircle)) {
|
|
rx = ry = ellipseOrCircle.r.baseVal.value;
|
|
} else {
|
|
rx = ellipseOrCircle.rx.baseVal.value;
|
|
ry = ellipseOrCircle.ry.baseVal.value;
|
|
}
|
|
|
|
var px = cx - point.x;
|
|
var py = cy - point.y;
|
|
|
|
var det = Math.sqrt(rx * rx * py * py + ry * ry * px * px);
|
|
|
|
var dx = Math.abs(rx * ry * px / det);
|
|
if (point.x < cx) {
|
|
dx = -dx;
|
|
}
|
|
var dy = Math.abs(rx * ry * py / det);
|
|
if (point.y < cy) {
|
|
dy = -dy;
|
|
}
|
|
|
|
return {x: cx + dx, y: cy + dy};
|
|
}
|
|
|
|
function sameSign(r1, r2) {
|
|
return r1 * r2 > 0;
|
|
}
|
|
|
|
// Add point to the found intersections, but check first that it is unique.
|
|
function addPoint(x, y, intersections) {
|
|
if (!intersections.some(function (elm) { return elm[0] === x && elm[1] === y; })) {
|
|
intersections.push([x, y]);
|
|
}
|
|
}
|
|
|
|
function intersectLine(x1, y1, x2, y2, x3, y3, x4, y4, intersections) {
|
|
// Algorithm from J. Avro, (ed.) Graphics Gems, No 2, Morgan Kaufmann, 1994, p7 and p473.
|
|
|
|
var a1, a2, b1, b2, c1, c2;
|
|
var r1, r2 , r3, r4;
|
|
var denom, offset, num;
|
|
var x, y;
|
|
|
|
// Compute a1, b1, c1, where line joining points 1 and 2 is F(x,y) = a1 x + b1 y + c1 = 0.
|
|
a1 = y2 - y1;
|
|
b1 = x1 - x2;
|
|
c1 = (x2 * y1) - (x1 * y2);
|
|
|
|
// Compute r3 and r4.
|
|
r3 = ((a1 * x3) + (b1 * y3) + c1);
|
|
r4 = ((a1 * x4) + (b1 * y4) + c1);
|
|
|
|
// Check signs of r3 and r4. If both point 3 and point 4 lie on
|
|
// same side of line 1, the line segments do not intersect.
|
|
if ((r3 !== 0) && (r4 !== 0) && sameSign(r3, r4)) {
|
|
return /*DONT_INTERSECT*/;
|
|
}
|
|
|
|
// Compute a2, b2, c2 where line joining points 3 and 4 is G(x,y) = a2 x + b2 y + c2 = 0
|
|
a2 = y4 - y3;
|
|
b2 = x3 - x4;
|
|
c2 = (x4 * y3) - (x3 * y4);
|
|
|
|
// Compute r1 and r2
|
|
r1 = (a2 * x1) + (b2 * y1) + c2;
|
|
r2 = (a2 * x2) + (b2 * y2) + c2;
|
|
|
|
// Check signs of r1 and r2. If both point 1 and point 2 lie
|
|
// on same side of second line segment, the line segments do
|
|
// not intersect.
|
|
if ((r1 !== 0) && (r2 !== 0) && (sameSign(r1, r2))) {
|
|
return /*DONT_INTERSECT*/;
|
|
}
|
|
|
|
// Line segments intersect: compute intersection point.
|
|
denom = (a1 * b2) - (a2 * b1);
|
|
if (denom === 0) {
|
|
return /*COLLINEAR*/;
|
|
}
|
|
|
|
offset = Math.abs(denom / 2);
|
|
|
|
// The denom/2 is to get rounding instead of truncating. It
|
|
// is added or subtracted to the numerator, depending upon the
|
|
// sign of the numerator.
|
|
num = (b1 * c2) - (b2 * c1);
|
|
x = (num < 0) ? ((num - offset) / denom) : ((num + offset) / denom);
|
|
|
|
num = (a2 * c1) - (a1 * c2);
|
|
y = (num < 0) ? ((num - offset) / denom) : ((num + offset) / denom);
|
|
|
|
// lines_intersect
|
|
addPoint(x, y, intersections);
|
|
}
|
|
|
|
function intersectPolygon(node, polygon, point) {
|
|
var x1 = node.x;
|
|
var y1 = node.y;
|
|
var x2 = point.x;
|
|
var y2 = point.y;
|
|
|
|
var intersections = [];
|
|
var points = polygon.points;
|
|
|
|
var minx = 100000, miny = 100000;
|
|
for (var j = 0; j < points.numberOfItems; j++) {
|
|
var p = points.getItem(j);
|
|
minx = Math.min(minx, p.x);
|
|
miny = Math.min(miny, p.y);
|
|
}
|
|
|
|
var left = x1 - node.width / 2 - minx;
|
|
var top = y1 - node.height / 2 - miny;
|
|
|
|
for (var i = 0; i < points.numberOfItems; i++) {
|
|
var p1 = points.getItem(i);
|
|
var p2 = points.getItem(i < points.numberOfItems - 1 ? i + 1 : 0);
|
|
intersectLine(x1, y1, x2, y2, left + p1.x, top + p1.y, left + p2.x, top + p2.y, intersections);
|
|
}
|
|
|
|
if (intersections.length === 1) {
|
|
return {x: intersections[0][0], y: intersections[0][1]};
|
|
}
|
|
|
|
if (intersections.length > 1) {
|
|
// More intersections, find the one nearest to edge end point
|
|
intersections.sort(function(p, q) {
|
|
var pdx = p[0] - point.x,
|
|
pdy = p[1] - point.y,
|
|
distp = Math.sqrt(pdx * pdx + pdy * pdy),
|
|
|
|
qdx = q[0] - point.x,
|
|
qdy = q[1] - point.y,
|
|
distq = Math.sqrt(qdx * qdx + qdy * qdy);
|
|
|
|
return (distp < distq) ? -1 : (distp === distq ? 0 : 1);
|
|
});
|
|
return {x: intersections[0][0], y: intersections[0][1]};
|
|
} else {
|
|
console.log('NO INTERSECTION FOUND, RETURN NODE CENTER', node);
|
|
return node;
|
|
}
|
|
}
|
|
|
|
function isComposite(g, u) {
|
|
return 'children' in g && g.children(u).length;
|
|
}
|
|
|
|
function bind(func, thisArg) {
|
|
// For some reason PhantomJS occassionally fails when using the builtin bind,
|
|
// so we check if it is available and if not, use a degenerate polyfill.
|
|
if (func.bind) {
|
|
return func.bind(thisArg);
|
|
}
|
|
|
|
return function() {
|
|
return func.apply(thisArg, arguments);
|
|
};
|
|
}
|
|
|
|
function applyStyle(style, domNode) {
|
|
if (style) {
|
|
var currStyle = domNode.attr('style') || '';
|
|
domNode.attr('style', currStyle + '; ' + style);
|
|
}
|
|
}
|