Collision detection of nodes and links

Hi All,

I was hoping for some help on doing collision detection between nodes (specifically between their subparts) and links.

For clarification, my node template is a base panel with a text box above a picture part. The error image is a shape over the picture part and only shown if there is a collision.

So far this is what I have:

// check for node collisions.
function checkForNodeCollisions(diagram, nodesThatHaveMoved, deletedNodes) {
deletedNodes = (typeof deletedNodes === “undefined”) ? false : deletedNodes;

var allNodes = [];
var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (!(node.collisionSet)) { // not defined, so define it as a Set of Parts.
        node.collisionSet = new go.Set(go.Part);
    }
    allNodes.push(node);
}

for (var i = 0; i < allNodes.length; i++) {
    var nodeA = allNodes<em>;
    var pictureA = nodeA.findObject("nodePart");
    for (var j = i; j < allNodes.length; j++) {
        var nodeB = allNodes[j];
        var pictureB = nodeB.findObject("nodePart");
        // don't check itself
        if (nodeA !== nodeB) {
            if (pictureA !== null && pictureB !== null) {
                if (pictureA.actualBounds.intersectsRect(pictureB.actualBounds) &&
                deletedNodes !== true) {
                    // nodes in owner groups will always intersect so check for them.
                    if (!isNodeInGroup(nodeA, nodeB)) {
                        nodeA.collisionSet.add(nodeB);
                        nodeB.collisionSet.add(nodeA);
                    }
                }
                else {
                    if (nodeA instanceof go.Group) {
                        var groupNodes = nodeA.memberParts;
                        while (groupNodes.next()) {
                            var n = groupNodes.value;
                            if (n instanceof go.Node) { // only remove nodes not, links.
                                nodeB.collisionSet.remove(n);
                            }
                        }
                    }
                    nodeA.collisionSet.remove(nodeB);
                    nodeB.collisionSet.remove(nodeA);
                }
            }
        }
    }

    var links = diagram.links;
    while (links.next()) {
        var link = links.value;
        var checkIntersection = true;
        if (link.fromNode !== null && link.fromNode === nodeA ||
            link.toNode !== null && link.toNode === nodeA) {
            checkIntersection = false;
            nodeA.collisionSet.remove(link);
        }
        if (checkIntersection) {

// TODO: Get link path here and test for intersection with picture.
if (pictureA !== null && pictureA.actualBounds.intersectsRect(link.actualBounds)) {
nodeA.collisionSet.add(link);
}
else {
nodeA.collisionSet.remove(link);
}
}
}
}

diagram.model.startTransaction("change collision colour");

var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (node.collisionSet.count > 0) {
        if (node instanceof go.Group) {
            var containedNodes = node.memberParts;
            var groupShape = node.findObject("groupSHAPE");  // case sensitive!
            if (groupShape !== null) {
                groupShape.fill = "rgba(255,0,0,0.4)";
                groupShape.stroke = "gray";
            }
            while (containedNodes.next()) {
                var n = containedNodes.value;
                var errorImage = n.findObject("errorImage");
                if (errorImage !== null) {
                    errorImage.visible = true;
                }
            }
        }
        else {
            var errorImage = node.findObject("errorImage");
            if (errorImage !== null) {
                errorImage.visible = true;
            }
        }
    }
    else {
        if (node instanceof go.Group) {
            var containedNodes = node.memberParts;
            var groupShape = node.findObject("groupSHAPE"); // case sensitive!
            if (groupShape !== null) {
                groupShape.fill = "rgba(128,128,128,0.2)";
                groupShape.stroke = "rgba(128,128,128,0.2)";
            }
            while (containedNodes.next()) {
                var n = containedNodes.value;
                var errorImage = n.findObject("errorImage");
                if (errorImage !== null) {
                    errorImage.visible = false;
                }
            }
        }
        else {
            var errorImage = node.findObject("errorImage");
            if (errorImage !== null) {
                errorImage.visible = false;
            }
        }
    }
}

diagram.model.commitTransaction("change collision colour");

};

// check if this node is in this group.
function isNodeInGroup(node1, node2) {
if (!(node1 instanceof go.Group) && !(node2 instanceof go.Group)) {
return false;
}
else if ((node1 instanceof go.Group) && !(node2 instanceof go.Group)) {
return isNodeInthisGroup(node2, node1);
}
else if (!(node1 instanceof go.Group) && (node2 instanceof go.Group)) {
return isNodeInthisGroup(node1, node2);
}
return false;
};

/// check if this node in contained by this group.
function isNodeInthisGroup(node, group) {
if (node.containingGroup === group) {
// tODO Check for nested groups higher up?
return true;
}
return false;
};

However I currently have some problems:

  • The picture.actualBounds returns the co-ordinates of the part in the node base panel, not in document co-ordinates. I need a clever way of translating the points into document co-ordinates but I can't think of one yet.
  • the link.actualBounds returns the bounding box around the whole link, but I would like to actually get the intersection of the path segments with the node picture rect. This also requires transforming the line segments into document co-ordinates. And this obviously gets more tricky when the link is a Bezier curve!
  • How would this work fro groups, specifically nested groups? My grouping check currently one goes one level and will probably clash with the links of nodes that the group contains.
I think GraphObject.getDocumentPoint should be able to help, but I'm not sure exactly how to use it.

Thanks.

Jonathan

Ok, so I have figured out how to transform the subpanels into document co-ordinates, using the nifty GraphObject.getDocumentPoint method. GoJS has thought of pretty much everything, good work!

So this is what I have so far:

// check for node collisions.
function checkForNodeCollisions(diagram, nodesThatHaveMoved, deletedNodes) {
deletedNodes = (typeof deletedNodes === “undefined”) ? false : deletedNodes;

var allNodes = [];
var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (!(node.collisionSet)) { // not defined, so define it as a Set of Parts.
        node.collisionSet = new go.Set(go.Part);
    }
    allNodes.push(node);
}

for (var i = 0; i < allNodes.length; i++) {
    var nodeA = allNodes<em>;
    var partA = getIntersectionPartForNode(nodeA);
    var intersectionARect = getDocumentRectForThisPart(partA);
    for (var j = i; j < allNodes.length; j++) {
        var nodeB = allNodes[j];
        var partB = getIntersectionPartForNode(nodeB);
        var intersectionBRect = getDocumentRectForThisPart(partB);
        // don't check itself
        if (nodeA !== nodeB) {
            if (intersectionARect !== null && intersectionBRect !== null) {
                if (intersectionARect.intersectsRect(intersectionBRect) &&
                deletedNodes !== true) {
                    // nodes in owner groups will always intersect so check for them.
                    if (!isNodeInGroup(nodeA, nodeB)) {
                        nodeA.collisionSet.add(nodeB);
                        nodeB.collisionSet.add(nodeA);
                    }
                }
                else {
                    if (nodeA instanceof go.Group) {
                        var groupNodes = nodeA.memberParts;
                        while (groupNodes.next()) {
                            var n = groupNodes.value;
                            if (n instanceof go.Node) { // only remove nodes not, links.
                                nodeB.collisionSet.remove(n);
                            }
                        }
                    }
                    nodeA.collisionSet.remove(nodeB);
                    nodeB.collisionSet.remove(nodeA);
                }
            }
        }
    }

    var links = diagram.links;
    while (links.next()) {
        var link = links.value;
        var checkIntersection = true;
        if (link.fromNode !== null && link.fromNode === nodeA ||
            link.toNode !== null && link.toNode === nodeA) {
            checkIntersection = false;
            nodeA.collisionSet.remove(link);
        }
        if (checkIntersection) {
            if (intersectionARect !== null && intersectionARect.intersectsRect(link.actualBounds)) {
                nodeA.collisionSet.add(link);
            }
            else {
                nodeA.collisionSet.remove(link);
            }
        }
    }
}

diagram.model.startTransaction("change collision colour");

var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (node.collisionSet.count > 0) {
        if (node instanceof go.Group) {
            var containedNodes = node.memberParts;
            var groupShape = node.findObject("groupShape");  // case sensitive!
            if (groupShape !== null) {
                groupShape.fill = "rgba(255,0,0,0.4)";
                groupShape.stroke = "gray";
            }
            while (containedNodes.next()) {
                var n = containedNodes.value;
                var errorImage = n.findObject("errorImage");
                if (errorImage !== null) {
                    errorImage.visible = true;
                }
            }
        }
        else {
            var errorImage = node.findObject("errorImage");
            if (errorImage !== null) {
                errorImage.visible = true;
            }
        }
    }
    else {
        if (node instanceof go.Group) {
            var containedNodes = node.memberParts;
            var groupShape = node.findObject("groupShape"); // case sensitive!
            if (groupShape !== null) {
                groupShape.fill = "rgba(128,128,128,0.2)";
                groupShape.stroke = "rgba(128,128,128,0.2)";
            }
            while (containedNodes.next()) {
                var n = containedNodes.value;
                var errorImage = n.findObject("errorImage");
                if (errorImage !== null) {
                    errorImage.visible = false;
                }
            }
        }
        else {
            var errorImage = node.findObject("errorImage");
            if (errorImage !== null) {
                errorImage.visible = false;
            }
        }
    }
}

diagram.model.commitTransaction("change collision colour");

};

function getIntersectionPartForNode(node) {
var intersectionPart = null;
var partLabel = “nodePart”;
if (node instanceof go.Group) {
partLabel = “groupShape”;
}
intersectionPart = node.findObject(partLabel);
return intersectionPart;
}

function getDocumentRectForThisPart(part) {
var point = part.getDocumentPoint(new go.Spot());
var documentRect = new go.Rect(point, part.actualBounds.size);
return documentRect;
};

I still need to handle when links are attached to nodes within groups, and to check the links actual path instead of the bounding box, but I am getting somewhere

Any suggestions would be greatly appreciated!

thanks.

Jonathan

It sounds like you could have used the Diagram.findObjectsIn method. You would use function (o) { return o.part; } as the second argument, and you would use a predicate like function (part) { return part instanceof go.Node; } as the third argument, or something more specific depending on what you are looking for. Then you can dive deeper into the resulting Nodes as needed.

Regarding links, use the Link.points to get the Points of the link’s route in document coordinates. That will work well enough for non-Bezier links. For Bezier links, it is too complicated to tell you here.

Trivial point: new go.Spot() is better expressed as the constant go.Spot.TopLeft.

Hi Walter,

Many thanks for your response. For me it seems the link.points for Bezier links works well enough for me, with routing.AvoidsNodes. The intersection is possibly a little bit rough, but good enough for my purposes.

I have not implemented your solution of using the Diagram.findObjectsIn method, as to be honest, I didn’t understand it well enough. the only thing outstanding is the ability to handle nested groups, which i am still working on.


I am happy with my current solution for now, as I am pressed for time, but perhaps I will revisit it if performance becomes an issue.


My updated code is below if you have any further suggestions:

// check for node collisions.
function checkForNodeCollisions(diagram, nodesThatHaveMoved, deletedNodes) {
deletedNodes = (typeof deletedNodes === “undefined”) ? false : deletedNodes;

var allNodes = [];
var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (!(node.collisionSet)) { // not defined, so define it as a Set of Parts.
        node.collisionSet = new go.Set(go.Part);
    }
    allNodes.push(node);
}

for (var i = 0; i < allNodes.length; i++) {
    var nodeA = allNodes<em>;
    var partA = getIntersectionPartForNode(nodeA);
    var intersectionARect = getDocumentRectForThisPart(partA);
    for (var j = i; j < allNodes.length; j++) {
        var nodeB = allNodes[j];
        var partB = getIntersectionPartForNode(nodeB);
        var intersectionBRect = getDocumentRectForThisPart(partB);
        // don't check itself
        if (nodeA !== nodeB) {
            if (intersectionARect !== null && intersectionARect.isReal() &&
                intersectionBRect !== null && intersectionBRect.isReal()) {
                if (intersectionARect.intersectsRect(intersectionBRect) &&
                deletedNodes !== true) {
                    // nodes in owner groups will always intersect so check for them.
                    if (!isNodeInGroup(nodeA, nodeB)) {
                        nodeA.collisionSet.add(nodeB);
                        nodeB.collisionSet.add(nodeA);
                    }
                }
                else {
                    if (nodeA instanceof go.Group) {
                        var groupNodes = nodeA.memberParts;
                        while (groupNodes.next()) {
                            var n = groupNodes.value;
                            if (n instanceof go.Node) { // only remove nodes not, links.
                                nodeB.collisionSet.remove(n);
                            }
                        }
                    }
                    nodeA.collisionSet.remove(nodeB);
                    nodeB.collisionSet.remove(nodeA);
                }
            }
        }
    }

    if (!(nodeA instanceof go.Group)) {
        var links = diagram.links;
        while (links.next()) {
            var link = links.value;
            var checkIntersection = true;
            if (link.fromNode !== null && link.fromNode === nodeA ||
                link.toNode !== null && link.toNode === nodeA) {
                checkIntersection = false;
                nodeA.collisionSet.remove(link);
            }
            if (checkIntersection) {
                var linkIntersects = false;

                if (intersectionARect !== null) {
                    linkIntersects = checkIfLinkIntersectsRect(link, intersectionARect);
                }

                if (linkIntersects) {
                    nodeA.collisionSet.add(link);
                }
                else {
                    nodeA.collisionSet.remove(link);
                }
            }
        }
    }
}

updateCollisionColours(diagram);

};

function getIntersectionPartForNode(node) {
var intersectionPart = null;
var partLabel = “nodePart”;
if (node instanceof go.Group) {
partLabel = “groupShape”;
}
intersectionPart = node.findObject(partLabel);
return intersectionPart;
};

function getDocumentRectForThisPart(part) {
var point = part.getDocumentPoint(go.Spot.TopLeft);
var documentRect = new go.Rect(point, part.actualBounds.size);
return documentRect;
};

function checkIfLinkIntersectsRect(link, rect) {
var linkIntresectsRect = false;
var linkPoints = link.points.toArray();

for (var i = 0; i < linkPoints.length; i++) {
    if (i + 1 < linkPoints.length) {
        var p1 = linkPoints<em>;
        var p2 = linkPoints[i + 1];
        if (lineIntersectsRect(p1, p2, rect)) {
            linkIntresectsRect = true;
            break;
        }
    }
}
return linkIntresectsRect;

}

function lineIntersectsRect(p1, p2, r) {
var tlPoint = new go.Point(r.x, r.y);
var trPoint = new go.Point(r.x + r.width, r.y);
var blPoint = new go.Point(r.x, r.y + r.height);
var brPoint = new go.Point(r.x + r.width, r.y + r.height);

var topLine = lineIntersectsLine(p1, p2, tlPoint, trPoint);
var rightLine = lineIntersectsLine(p1, p2, trPoint, brPoint);
var bottomLine = lineIntersectsLine(p1, p2, brPoint, blPoint);
var leftLine = lineIntersectsLine(p1, p2, blPoint, trPoint);

return topLine || rightLine || bottomLine || leftLine || (r.containsPoint(p1) || r.containsPoint(p2));

};

function lineIntersectsLine(l1p1, l1p2, l2p1, l2p2) {
var q = (l1p1.y - l2p1.y) * (l2p2.x - l2p1.x) - (l1p1.x - l2p1.x) * (l2p2.y - l2p1.y);
var d = (l1p2.x - l1p1.x) * (l2p2.y - l2p1.y) - (l1p2.y - l1p1.y) * (l2p2.x - l2p1.x);

if (d === 0) {
    return false;
}

var r = q / d;

q = (l1p1.y - l2p1.y) * (l1p2.x - l1p1.x) - (l1p1.x - l2p1.x) * (l1p2.y - l1p1.y);
var s = q / d;

if (r < 0 || r > 1 || s < 0 || s > 1) {
    return false;
}

return true;

};

// check if this node is in this group.
function isNodeInGroup(node1, node2) {
if (!(node1 instanceof go.Group) && !(node2 instanceof go.Group)) {
return false;
}
else if ((node1 instanceof go.Group) && !(node2 instanceof go.Group)) {
return isNodeInthisGroup(node2, node1);
}
else if (!(node1 instanceof go.Group) && (node2 instanceof go.Group)) {
return isNodeInthisGroup(node1, node2);
}
return false;
};

/// check if this node in contained by this group.
function isNodeInthisGroup(node, group) {
if (node.containingGroup === group) {
// tODO Check for nested groups higher up?
return true;
}
return false;
};

function updateCollisionColours(diagram) {
diagram.model.startTransaction(“change collision colour”);

var nodes = diagram.nodes;
while (nodes.next()) {
    var node = nodes.value;
    if (node instanceof go.Group) {
        var containedNodes = node.memberParts;
        toggleGroupCollision(node);
        while (containedNodes.next()) {
            var n = containedNodes.value;
            if (n instanceof go.Node) {
                toggleNodeCollision(n);
            }
        }
    }
    else {
        toggleNodeCollision(node);
    }
}

diagram.model.commitTransaction("change collision colour");

};

function toggleGroupCollision(group) {
var groupCollision = group.collisionSet.count > 0;
var groupShape = group.findObject(“groupShape”); // case sensitive!
if (groupShape !== null) {
if (groupCollision) {
groupShape.fill = “rgba(255,0,0,0.4)”;
groupShape.stroke = “rgba(255,0,0,0.4)”;
}
else {
groupShape.fill = “rgba(128,128,128,0.2)”;
groupShape.stroke = “rgba(128,128,128,0.2)”;
}
}
};

function toggleNodeCollision(node) {
var nodeCollision = node.collisionSet.count > 0;
var errorImage = node.findObject(“errorImage”);
if (errorImage !== null) {
errorImage.visible = nodeCollision;
}
};

Thanks.

Jonathan

Look at Part.containingGroup to see if a Part is an immediate member of a Group. Use Part.isMemberOf to decide whether a Node is a member of a Group, perhaps with intervening Groups.

Iterate over Group.memberParts to look at all of the immediate Parts of a Group. But you can easily recurse if you want to look into nested Groups. Also consider using Group.findSubGraphParts() to find all of the possibly nested member Parts.

Thanks Walter, these suggestions are great! The isMemberOf is exactly what I needed, and now nested groups exactly the way I want. However the customer may have a different idea, so I may to change it later! Tongue

Thanks again for the help!