forked from Open-CT/openct-tasks
522 lines
17 KiB
JavaScript
522 lines
17 KiB
JavaScript
function VisualGraph(id, paper, graph, graphDrawer, autoDraw, vertexVisualInfo, edgeVisualInfo) {
|
|
this.id = id;
|
|
this.paper = paper;
|
|
this.graph = graph;
|
|
this.graphDrawer = graphDrawer;
|
|
this.hasDrawing = false;
|
|
|
|
if(vertexVisualInfo) {
|
|
this.vertexVisualInfo = vertexVisualInfo;
|
|
}
|
|
else {
|
|
this.vertexVisualInfo = {};
|
|
}
|
|
if(edgeVisualInfo) {
|
|
this.edgeVisualInfo = edgeVisualInfo;
|
|
}
|
|
else {
|
|
this.edgeVisualInfo = {};
|
|
}
|
|
this.edgeRaphaels = {};
|
|
this.vertexRaphaels = {};
|
|
var self = this;
|
|
|
|
this.listener = {
|
|
addVertex: function(id, info) {
|
|
self._drawVertex(id, info);
|
|
return true;
|
|
},
|
|
addEdge: function(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo) {
|
|
self._drawEdge(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo);
|
|
return true;
|
|
},
|
|
removeVertex: function(id, info) {
|
|
self._eraseVertex(id, info);
|
|
delete self.vertexVisualInfo[id];
|
|
return true;
|
|
},
|
|
removeEdge: function(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo) {
|
|
self._eraseEdge(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo);
|
|
delete self.edgeVisualInfo[id];
|
|
return true;
|
|
}
|
|
};
|
|
this.priority = 1000;
|
|
|
|
this._attachRaphaelID = function(elements, id) {
|
|
for(var iElement in elements) {
|
|
elements[iElement].data("id", id);
|
|
}
|
|
};
|
|
|
|
this._initDrawer = function() {
|
|
if(!this.hasDrawing && this.graphDrawer.init) {
|
|
this.graphDrawer.init(this.paper, this.graph, this);
|
|
}
|
|
this.hasDrawing = true;
|
|
};
|
|
|
|
this.setAutoDraw = function(autoDraw) {
|
|
if(autoDraw == this.autoDraw) {
|
|
return;
|
|
}
|
|
this.autoDraw = autoDraw;
|
|
if(autoDraw) {
|
|
this.graph.addPostListener(this.id, this.listener, this.priority);
|
|
}
|
|
else {
|
|
this.graph.removePostListener(this.id);
|
|
}
|
|
};
|
|
|
|
this.redraw = function() {
|
|
this._removeGraphics();
|
|
var vertices = this.graph.getAllVertices();
|
|
for(var iVertex in vertices) {
|
|
this._drawVertex(vertices[iVertex], this.graph.getVertexInfo(vertices[iVertex]));
|
|
}
|
|
var edges = this.graph.getAllEdges();
|
|
for(var iEdge in edges) {
|
|
var edgeVertices = this.graph.getEdgeVertices(edges[iEdge]);
|
|
var vertex1 = edgeVertices[0];
|
|
var vertex2 = edgeVertices[1];
|
|
var vertex1Info = this.graph.getVertexInfo(vertex1);
|
|
var vertex2Info = this.graph.getVertexInfo(vertex2);
|
|
var edgeInfo = this.graph.getEdgeInfo(edges[iEdge]);
|
|
this._drawEdge(edges[iEdge], vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo);
|
|
}
|
|
if(this.graphDrawer.drawingComplete) {
|
|
this.graphDrawer.drawingComplete();
|
|
}
|
|
};
|
|
|
|
this._removeGraphics = function() {
|
|
if(!this.hasDrawing) {
|
|
return;
|
|
}
|
|
for(var edgeID in this.edgeRaphaels) {
|
|
this._eraseEdge(edgeID);
|
|
}
|
|
for(var vertexID in this.vertexRaphaels) {
|
|
this._eraseVertex(vertexID);
|
|
}
|
|
if(this.graphDrawer.deinit) {
|
|
this.graphDrawer.deinit();
|
|
}
|
|
this.hasDrawing = false;
|
|
};
|
|
|
|
this._drawVertex = function(id, info) {
|
|
self._initDrawer();
|
|
if(!self.vertexVisualInfo[id]) {
|
|
self.vertexVisualInfo[id] = {};
|
|
}
|
|
if(self.graphDrawer.drawVertex) {
|
|
self.vertexRaphaels[id] = self.graphDrawer.drawVertex(id, info, self.vertexVisualInfo[id]);
|
|
self._attachRaphaelID(self.vertexRaphaels[id], id);
|
|
}
|
|
else {
|
|
self.vertexRaphaels[id] = [];
|
|
}
|
|
};
|
|
|
|
this._drawEdge = function(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo) {
|
|
self._initDrawer();
|
|
if(!self.edgeVisualInfo[id]) {
|
|
self.edgeVisualInfo[id] = {};
|
|
}
|
|
if(self.graphDrawer.drawEdge) {
|
|
self.edgeRaphaels[id] = self.graphDrawer.drawEdge(id, vertex1, vertex2, vertex1Info, vertex2Info, self.vertexVisualInfo[vertex1], self.vertexVisualInfo[vertex2], edgeInfo, self.edgeVisualInfo[id]);
|
|
self._attachRaphaelID(self.edgeRaphaels[id], id);
|
|
}
|
|
else {
|
|
self.edgeRaphaels[id] = [];
|
|
}
|
|
};
|
|
|
|
this._eraseVertex = function(id, info) {
|
|
if(this.graphDrawer.eraseVertex) {
|
|
this.graphDrawer.eraseVertex(id, info, self.vertexVisualInfo[id]);
|
|
}
|
|
for(var iElement in this.vertexRaphaels[id]) {
|
|
var element = this.vertexRaphaels[id][iElement];
|
|
if(element && element.remove) {
|
|
element.remove();
|
|
}
|
|
}
|
|
delete self.vertexRaphaels[id];
|
|
};
|
|
|
|
this._eraseEdge = function(id, vertex1, vertex2, vertex1Info, vertex2Info, edgeInfo) {
|
|
if(this.graphDrawer.eraseEdge) {
|
|
this.graphDrawer.eraseEdge(id, vertex1, vertex2, vertex1Info, vertex2Info, self.vertexVisualInfo[vertex1], self.vertexVisualInfo[vertex2], edgeInfo, self.edgeVisualInfo[id]);
|
|
}
|
|
for(var iElement in this.edgeRaphaels[id]) {
|
|
var element = this.edgeRaphaels[id][iElement];
|
|
if(element && element.remove) {
|
|
element.remove();
|
|
}
|
|
}
|
|
delete self.edgeRaphaels[id];
|
|
};
|
|
|
|
this.remove = function() {
|
|
this._removeGraphics();
|
|
this.setAutoDraw(false);
|
|
};
|
|
|
|
this.pushVertexRaphael = function(id, element) {
|
|
if(!this.vertexRaphaels[id]) {
|
|
return;
|
|
}
|
|
this.vertexRaphaels[id].push(element);
|
|
element.data("id", id);
|
|
};
|
|
|
|
this.popVertexRaphael = function(id) {
|
|
if(!this.vertexRaphaels[id] || !this.vertexRaphaels[id].length) {
|
|
return;
|
|
}
|
|
return this.vertexRaphaels[id].pop();
|
|
};
|
|
|
|
this.pushEdgeRaphael = function(id, element) {
|
|
if(!this.edgeRaphaels[id]) {
|
|
return;
|
|
}
|
|
this.edgeRaphaels[id].push(element);
|
|
element.data("id", id);
|
|
};
|
|
|
|
this.popEdgeRaphael = function(id) {
|
|
if(!this.edgeRaphaels[id] || !this.edgeRaphaels[id].length) {
|
|
return;
|
|
}
|
|
return this.edgeRaphaels[id].pop();
|
|
};
|
|
|
|
this.setVertexVisualInfo = function(id, info) {
|
|
this.vertexVisualInfo[id] = info;
|
|
};
|
|
|
|
this.getVertexVisualInfo = function(id) {
|
|
return this.vertexVisualInfo[id];
|
|
};
|
|
|
|
this.setEdgeVisualInfo = function(id, info) {
|
|
this.edgeVisualInfo[id] = info;
|
|
};
|
|
|
|
this.getEdgeVisualInfo = function(id) {
|
|
return this.edgeVisualInfo[id];
|
|
};
|
|
|
|
this.getRaphaelsFromID = function(id) {
|
|
if(this.vertexRaphaels[id]) {
|
|
return this.vertexRaphaels[id];
|
|
}
|
|
if(this.edgeRaphaels[id]) {
|
|
return this.edgeRaphaels[id];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
this.elementToFront = function(id) {
|
|
var raphaels = this.getRaphaelsFromID(id);
|
|
for(var iElement in raphaels) {
|
|
raphaels[iElement].toFront();
|
|
}
|
|
};
|
|
|
|
this.setPaper = function(paper) {
|
|
this.paper = paper;
|
|
};
|
|
this.getPaper = function() {
|
|
return this.paper;
|
|
};
|
|
|
|
this.setDrawer = function(graphDrawer) {
|
|
this.graphDrawer = graphDrawer;
|
|
};
|
|
|
|
this.getGraph = function() {
|
|
return this.graph;
|
|
};
|
|
|
|
this.toJSON = function() {
|
|
return JSON.stringify({
|
|
vertexVisualInfo: this.vertexVisualInfo,
|
|
edgeVisualInfo: this.edgeVisualInfo,
|
|
minGraph: this.graph.toMinimized()
|
|
});
|
|
};
|
|
|
|
this.setAutoDraw(autoDraw);
|
|
if(autoDraw) {
|
|
this.redraw();
|
|
}
|
|
}
|
|
|
|
VisualGraph.fromJSON = function(visualGraphStr, id, paper, graph, graphDrawer, autoDraw) {
|
|
var visualInfo = JSON.parse(visualGraphStr);
|
|
if(!graph) {
|
|
graph = Graph.fromMinimized(visualInfo.minGraph);
|
|
}
|
|
return new VisualGraph(id, paper, graph, graphDrawer, autoDraw, visualInfo.vertexVisualInfo, visualInfo.edgeVisualInfo);
|
|
};
|
|
|
|
function SimpleGraphDrawer(circleAttr, lineAttr, vertexDrawer, autoMove, vertexMover, thickMode, innerLineAttr) {
|
|
this.circleAttr = circleAttr;
|
|
this.lineAttr = lineAttr;
|
|
this.init = function(paper, graph, visualGraph) {
|
|
this.paper = paper;
|
|
this.graph = graph;
|
|
this.visualGraph = visualGraph;
|
|
this.customElements = {};
|
|
this.originalPositions = {};
|
|
};
|
|
this.drawVertex = function(id, info, visualInfo) {
|
|
var pos = this._getVertexPosition(visualInfo);
|
|
this.originalPositions[id] = pos;
|
|
|
|
var result = [this.paper.circle(pos.x, pos.y).attr(this.circleAttr)];
|
|
if(vertexDrawer) {
|
|
var raphaels = vertexDrawer(id, info, pos.x, pos.y);
|
|
this._addCustomElements(id, raphaels);
|
|
result = result.concat(raphaels);
|
|
}
|
|
return result;
|
|
};
|
|
this.setDrawVertex = function(fct) {
|
|
this.drawVertex = fct;
|
|
};
|
|
this.drawEdge = function(id, vertex1, vertex2, vertex1Info, vertex2Info, vertex1VisualInfo, vertex2VisualInfo, edgeInfo, edgeVisualInfo) {
|
|
if(thickMode) {
|
|
var path = this._getThickEdgePath(vertex1, vertex2);
|
|
return [this.paper.path(path).attr(this.lineAttr).toBack(), this.paper.path(path).attr(innerLineAttr)];
|
|
}
|
|
else {
|
|
return [this.paper.path(this._getEdgePath(vertex1, vertex2)).attr(this.lineAttr).toBack()];
|
|
}
|
|
};
|
|
this.setDrawEdge = function(fct) {
|
|
this.drawEdge = fct;
|
|
};
|
|
this._getVertexPosition = function(visualInfo) {
|
|
if(visualInfo.x === undefined || visualInfo.x === null) {
|
|
visualInfo.x = 0;
|
|
visualInfo.y = 0;
|
|
}
|
|
return {
|
|
x: visualInfo.x,
|
|
y: visualInfo.y
|
|
};
|
|
};
|
|
this.getVertexPosition = function(id) {
|
|
return this._getVertexPosition(this.visualGraph.getVertexVisualInfo(id));
|
|
};
|
|
this._addCustomElements = function(id, raphaels) {
|
|
// Save original attributes. This allows us to move the object later by transformation.
|
|
this.customElements[id] = [];
|
|
for(var iElement in raphaels) {
|
|
var raphael = raphaels[iElement];
|
|
this.customElements[id].push({
|
|
raphael: raphael,
|
|
originalAttrs: $.extend(true, {}, raphael.attrs)
|
|
});
|
|
}
|
|
};
|
|
this.moveVertex = function(id, x, y) {
|
|
var info = this.visualGraph.getVertexVisualInfo(id);
|
|
info.x = x;
|
|
info.y = y;
|
|
var raphaels = this.visualGraph.getRaphaelsFromID(id);
|
|
raphaels[0].attr({
|
|
cx: x,
|
|
cy: y
|
|
});
|
|
|
|
// Move the custom Raphael objects.
|
|
if(vertexMover) {
|
|
vertexMover(id, raphaels, x, y);
|
|
}
|
|
if(autoMove) {
|
|
this._moveCustomElements(id, x, y);
|
|
}
|
|
|
|
var childrenIDs = this.graph.getChildren(id);
|
|
for(var iChild in childrenIDs) {
|
|
this.refreshEdgePosition(id, childrenIDs[iChild]);
|
|
}
|
|
if(this.graph.directed) {
|
|
var parentIDs = this.graph.getParents(id);
|
|
for(var iParent in parentIDs) {
|
|
this.refreshEdgePosition(parentIDs[iParent], id);
|
|
}
|
|
}
|
|
};
|
|
this._moveCustomElements = function(id, x, y) {
|
|
var elements = this.customElements[id];
|
|
var transformation = ["T", x - this.originalPositions[id].x, y - this.originalPositions[id].y];
|
|
for(var iElement in elements) {
|
|
var element = elements[iElement];
|
|
// Paths get transformed using Raphael.transformPath,
|
|
// for compatibility. Other objects get transformed normally.
|
|
if(element.raphael.type === "path") {
|
|
element.raphael.attr({path: Raphael.transformPath(element.originalAttrs.path, transformation)});
|
|
}
|
|
else {
|
|
element.raphael.transform(transformation);
|
|
}
|
|
}
|
|
};
|
|
this.refreshEdgePosition = function(vertex1, vertex2) {
|
|
var edges = this.graph.getEdgesFrom(vertex1, vertex2);
|
|
var info1 = this.visualGraph.getVertexVisualInfo(vertex1);
|
|
var info2 = this.visualGraph.getVertexVisualInfo(vertex2);
|
|
var newPath;
|
|
if(thickMode) {
|
|
newPath = this._getThickEdgePath(vertex1, vertex2);
|
|
}
|
|
else {
|
|
newPath = this._getEdgePath(vertex1, vertex2);
|
|
}
|
|
for(var iEdge in edges) {
|
|
var edgeID = edges[iEdge];
|
|
var raphaels = this.visualGraph.getRaphaelsFromID(edgeID);
|
|
raphaels[0].attr("path", newPath);
|
|
if(thickMode) {
|
|
raphaels[1].attr("path", newPath);
|
|
}
|
|
}
|
|
};
|
|
this._getEdgePath = function(vertex1, vertex2) {
|
|
var info1 = this.visualGraph.getVertexVisualInfo(vertex1);
|
|
var info2 = this.visualGraph.getVertexVisualInfo(vertex2);
|
|
var x1 = info1.x, y1 = info1.y, x2 = info2.x, y2 = info2.y;
|
|
var r = this.circleAttr.r;
|
|
/*
|
|
* We want to draw an edge from the center of one circle toward the center
|
|
* of another, but only up to its surface. Otherwise the arrow would be
|
|
* inside the target circle.
|
|
* The line between centers goes from x1,y1 to x2,y2, and we want to
|
|
* chop length r from it. We call the denote by w,h the displacement
|
|
* from x2,y2.
|
|
*/
|
|
|
|
// Same X coordinate.
|
|
if(x1 == x2) {
|
|
if(y1 < y2) {
|
|
return ["M", x1, y1, "L", x2, y2 - r];
|
|
}
|
|
else {
|
|
return ["M", x1, y1, "L", x2, y2 + r];
|
|
}
|
|
}
|
|
// Swap for convenience. x1,y1 is always to the left.
|
|
var swap = false;
|
|
if(x1 > x2) {
|
|
swap = true;
|
|
var temp = x1;
|
|
x1 = x2;
|
|
x2 = temp;
|
|
temp = y1;
|
|
y1 = y2;
|
|
y2 = temp;
|
|
}
|
|
// We have h^2 + w^2 = r^2 and (y2-y1)/(x2-x1) = h/w.
|
|
var slope = 1.0 * (y2 - y1) / (x2 - x1);
|
|
var w = (r / Math.sqrt((1 + slope * slope)));
|
|
var h = (slope * w);
|
|
if(!swap) {
|
|
return ["M", x1, y1, "L", x2 - w, y2 - h];
|
|
}
|
|
else {
|
|
return ["M", x2, y2, "L", x1 + w, y1 + h];
|
|
}
|
|
};
|
|
this._getThickEdgePath = function(vertex1, vertex2) {
|
|
var info1 = this.visualGraph.getVertexVisualInfo(vertex1);
|
|
var info2 = this.visualGraph.getVertexVisualInfo(vertex2);
|
|
var x1 = info1.x, y1 = info1.y, x2 = info2.x, y2 = info2.y;
|
|
return ["M", x1, y1, "L", x2, y2];
|
|
};
|
|
this.setCircleAttr = function(circleAttr) {
|
|
this.circleAttr = circleAttr;
|
|
};
|
|
this.setLineAttr = function(lineAttr) {
|
|
this.lineAttr = lineAttr;
|
|
};
|
|
this.reapplyAttr = function() {
|
|
var vertices = this.graph.getAllVertices();
|
|
for(var iVertex in vertices) {
|
|
this.visualGraph.getRaphaelsFromID(vertices[iVertex])[0].attr(this.circleAttr);
|
|
}
|
|
var edges = this.graph.getAllEdges();
|
|
for(var iEdge in edges) {
|
|
var raphaels = this.visualGraph.getRaphaelsFromID(edges[iEdge]);
|
|
raphaels[0].attr(this.lineAttr);
|
|
if(thickMode) {
|
|
raphaels[1].attr(innerLineAttr);
|
|
}
|
|
}
|
|
};
|
|
this.getDistanceFromVertex = function(id, xPos, yPos) {
|
|
var vertexPos = this.getVertexPosition(id);
|
|
var xDistance = xPos - vertexPos.x;
|
|
var yDistance = yPos - vertexPos.y;
|
|
var distanceFromCenter = Math.sqrt(xDistance * xDistance + yDistance * yDistance);
|
|
if(distanceFromCenter <= this.circleAttr.r) {
|
|
return 0;
|
|
}
|
|
return distanceFromCenter - this.circleAttr.r;
|
|
};
|
|
this.getDistanceFromEdge = function(id, xPos, yPos) {
|
|
var edgePath = this.visualGraph.getRaphaelsFromID(id)[0].attrs.path;
|
|
var x1, y1, x2, y2;
|
|
// In modern browsers the path is an array and we can get the endpoints
|
|
// directly. In old browsers it may be a comma separated string.
|
|
if($.isArray(edgePath)) {
|
|
if($.isArray(edgePath[0])) {
|
|
// Path a 2D array: [["M", x1, y1], ["L", x2, y2]]
|
|
x1 = parseInt(edgePath[0][1]);
|
|
y1 = parseInt(edgePath[0][2]);
|
|
x2 = parseInt(edgePath[1][1]);
|
|
y2 = parseInt(edgePath[1][2]);
|
|
}
|
|
else {
|
|
// Path is an array: ["M", x1, y1, "L", x2, y2]
|
|
x1 = parseInt(edgePath[1]);
|
|
y1 = parseInt(edgePath[2]);
|
|
x2 = parseInt(edgePath[4]);
|
|
y2 = parseInt(edgePath[5]);
|
|
}
|
|
}
|
|
else {
|
|
// Path is a string: "M,x1,y1,L,x2,y2"
|
|
var parts = edgePath.split(",");
|
|
x1 = parseInt(parts[1]);
|
|
y1 = parseInt(parts[2]);
|
|
x2 = parseInt(parts[4]);
|
|
y2 = parseInt(parts[5]);
|
|
}
|
|
return Math.sqrt(distanceToSegmentSquared(xPos, yPos, x1, y1, x2, y2));
|
|
};
|
|
|
|
function distanceSquared(x1, y1, x2, y2) {
|
|
return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
|
|
}
|
|
|
|
function distanceToSegmentSquared(xPos, yPos, x1, y1, x2, y2) {
|
|
// Use algorithm for distance between point and segment.
|
|
// See: https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/1501725#1501725
|
|
var lengthSquared = distanceSquared(x1, y1, x2, y2);
|
|
if(lengthSquared === 0) {
|
|
return distanceSquared(xPos, yPos, x1, y1);
|
|
}
|
|
var t = ((xPos - x1) * (x2 - x1) + (yPos - y1) * (y2 - y1)) / lengthSquared;
|
|
t = Math.max(0, Math.min(1, t));
|
|
return distanceSquared(xPos, yPos, x1 + t * (x2 - x1), y1 + t * (y2 - y1));
|
|
}
|
|
}
|