Link re-routing and port location

We have created a design based very closely on your flowchart sample.The flowchart sample DOES have the behavior that I want.

I am having a couple small problems that I can’t seem to resolve.

Problem 1: When link is added, the link is redrawn with different ports. Very annoying.
Problem 2: The ports on the Condition node (Diamond shape) appear inside the shape, not on the edges.

Below is the JS code from my page:

        function init() {  // init for these samples -- you don't need to call this            
            var $ = go.GraphObject.make;  // for conciseness in defining templates
            //IsReadOnly = parentWindow.IsReadOnly;
         
            switch (qsMode){
                case 0:
                    IsReadOnly = true;
                    IsDisabled = true;                
                    break;
                case 1:
                    IsReadOnly = true;
                    IsDisabled = false; 
                    break;
                case 2:
                    IsReadOnly = false;
                    IsDisabled = false; 
                    break;
                default:
                    break;
            }

            if ((hdnGoJSData.value != "")) {
                (jQuery)("#mySavedModel").val(hdnGoJSData.value);
            }

            goDiagram =
                $(go.Diagram, "myDiagram",  // must name or refer to the DIV HTML element
                    {
                        initialContentAlignment : go.Spot.TopCenter,
                        allowDrop: true,  // must be true to accept drops from the Palette
                        //    isReadOnly: IsReadOnly,
                        "LinkDrawn": showLinkLabel,  // this DiagramEvent listener is defined below
                        "LinkRelinked": showLinkLabel,
                        "undoManager.isEnabled": true  // enable undo & redo
                    });


            //Call the ajax to redraw the runtime diagram from the server values on initial load
            goDiagram.addDiagramListener("InitialLayoutCompleted", function (ev){
                var itmestatusjson = document.getElementById("<%= WorkflowItemStatus.ClientID %>");
                SetWorkflowItemState(itmestatusjson.value);
            });

            goDiagram.addDiagramListener("ObjectContextClicked", function (ev) {

                if (qsMode != 1) {

                    //set popup title, ev.subject.le is inner text
                    var selectedNode = goDiagram.selection.iterator.first();
                    var selectedNodeText = selectedNode.Qe.text;
                    if (selectedNodeText === 'undefined') {
                        return false;
                    }

                    // Only allow action items and decision items to show a popup in design mode
                    var selectedNodeCategory = selectedNode.Qe.category;
                    if (selectedNodeCategory === 'Start' || selectedNodeCategory === 'End' || selectedNodeCategory === 'Link' || selectedNodeCategory === 'Comment') {
                        return false;
                    }

                    // Set the title text (for both design and runtime popups)
                    (jQuery)("#lblNodeText").text(selectedNodeText);
                    (jQuery)("#lblNodeText2").text(selectedNodeText);
                    (jQuery)("#lblConditionNodeText").text(selectedNodeText);
                    (jQuery)("#lblComplianceNodeText").text(selectedNodeText);

                    if (selectedNodeCategory) {
                        OnSingleClick(selectedNodeCategory);
                    }
                }
                return false;
            });
            
            goDiagram.addDiagramListener("ObjectSingleClicked", function (ev) {
                if (qsMode == 1) {
                
                    //set popup title, ev.subject.le is inner text
                    var selectedNode = goDiagram.selection.iterator.first();
                    var selectedNodeText = selectedNode.Qe.text;
                    if (selectedNodeText === 'undefined') {
                        return false;
                    }

                    // Only allow action items and decision items to show a popup in design mode
                    var selectedNodeCategory = selectedNode.Qe.category;
                    if (selectedNodeCategory === 'Start' || selectedNodeCategory === 'End' || selectedNodeCategory === 'Link' || selectedNodeCategory === 'Comment') {
                        return false;
                    }

                    // Set the title text (for both design and runtime popups)
                    (jQuery)("#lblNodeText").text(selectedNodeText);
                    (jQuery)("#lblNodeText2").text(selectedNodeText);
                    (jQuery)("#lblConditionNodeText").text(selectedNodeText);
                    (jQuery)("#lblComplianceNodeText").text(selectedNodeText);

                    var selectedNodeKey = selectedNode.Qe.key;

                    if (selectedNodeCategory) {

                        var statusItem = null;
                        for (var k = 0; k < itemsarray.Items.Item.length; k++) {    
                            if (itemsarray.Items.Item[k].Key == selectedNodeKey) {
                                statusItem = itemsarray.Items.Item[k];
                                break;
                            }
                        }

                        if (statusItem.IsDisabled != "1") {
                            OnSingleClick(selectedNodeCategory);
                        }
                    }
                }
                return false;
            });

            function nodeStyle() {
                return [
                    // The Node.location comes from the "loc" property of the node data,
                    // converted by the Point.parse static method.
                    // If the Node.location is changed, it updates the "loc" property of the node data,
                    // converting back using the Point.stringify static method.
                    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
                    {
                        // the Node.location is at the center of each node
                        locationSpot: go.Spot.Center,
                        //isShadowed: true,
                        //shadowColor: "#888",
                        // handle mouse enter/leave events to show/hide the ports
                        mouseEnter: function (e, obj) { showPorts(obj.part, true); },
                        mouseLeave: function (e, obj) { showPorts(obj.part, false); },
                    }
                ];
            }

            // Define a function for creating a "port" that is normally transparent.
            // The "name" is used as the GraphObject.portId, the "spot" is used to control how links connect
            // and where the port is positioned on the node, and the boolean "output" and "input" arguments
            // control whether the user can draw links from or to the port.
            function makePort(name, spot, output, input) {
                // the port is basically just a small circle that has a white stroke when it is made visible
                return $(go.Shape, "Circle",
                    {
                        fill: "transparent",
                        stroke: null,  // this is changed to "white" in the showPorts function
                        desiredSize: new go.Size(8, 8),
                        alignment: spot, alignmentFocus: spot,  // align the port on the main Shape
                        portId: name,  // declare this object to be a "port"
                        fromSpot: spot, toSpot: spot,  // declare where links may connect at this port
                        fromLinkable: output, toLinkable: input,  // declare whether the user may draw links to/from here
                        cursor: "pointer"  // show a different cursor to indicate potential link point
                    });
            }

            // define the Node templates for regular nodes

            var lightText = 'whitesmoke';

            goDiagram.nodeTemplateMap.add("",  // the default category
                $(go.Node, "Spot", nodeStyle(),
                    // the main object is a Panel that surrounds a TextBlock with a rectangular Shape
                    $(go.Panel, "Auto",
                        $(go.Shape, "Rectangle", 
                        { fill: "#00A9C9", stroke: null}, 
                        new go.Binding("figure", "figure"),
                        new go.Binding("fill","fill").makeTwoWay()),
                        $(go.TextBlock,
                            {   name: "TEXTBLOCK",
                                font: "bold 11pt Helvetica, Arial, sans-serif",
                                stroke: lightText,
                                margin: 8,
                                maxSize: new go.Size(160, NaN),
                                wrap: go.TextBlock.WrapFit,
                                editable: true
                            },
                            new go.Binding("text", "text").makeTwoWay())
                ),
                // four named ports, one on each side:
                makePort("T", go.Spot.Top, false, true),
                makePort("L", go.Spot.Left, true, true),
                makePort("R", go.Spot.Right, true, true),
                makePort("B", go.Spot.Bottom, true, false)
            ));

            goDiagram.nodeTemplateMap.add("Start",
                $(go.Node, "Spot", nodeStyle(),
                    $(go.Panel, "Auto",
                        $(go.Shape, "Circle",
                            { minSize: new go.Size(40, 60), fill: "#79C900", stroke: null}),
                        $(go.TextBlock, "Start",
                            { margin: 5, font: "bold 11pt Helvetica, Arial, sans-serif", stroke: lightText }, 
                            new go.Binding("text", "text").makeTwoWay())
                    ),
                    // three named ports, one on each side except the top, all output only:
                    makePort("L", go.Spot.Left, true, false),
                    makePort("R", go.Spot.Right, true, false),
                    makePort("B", go.Spot.Bottom, true, false)
                ));

            goDiagram.nodeTemplateMap.add("End",
                $(go.Node, "Spot", nodeStyle(),
                    $(go.Panel, "Auto",
                        $(go.Shape, "Circle",
                            { minSize: new go.Size(40, 60), fill: "#DC3C00", stroke: null} ),
                        $(go.TextBlock, "End",
                            { margin: 5, font: "bold 11pt Helvetica, Arial, sans-serif", stroke: lightText }, new go.Binding("text", "text").makeTwoWay())
                    ),

                    // three named ports, one on each side except the bottom, all input only:
                    makePort("T", go.Spot.Top, false, true),
                    makePort("L", go.Spot.Left, false, true),
                    makePort("R", go.Spot.Right, false, true)
                ));

            goDiagram.nodeTemplateMap.add("Action",
                $(go.Node, "Auto", nodeStyle(),
                    $(go.Shape, "File",
                        { fill: "#EFFAB4", stroke: null},
                        new go.Binding("fill","fill").makeTwoWay()),
                    $(go.TextBlock,
                        {   name: "TEXTBLOCK",
                            margin: 5,
                            maxSize: new go.Size(200, NaN),
                            wrap: go.TextBlock.WrapFit,
                            textAlign: "center",
                            editable: true,
                            font: "bold 11pt Helvetica, Arial, sans-serif",
                            stroke: '#454545'
                        },
                        new go.Binding("text", "text").makeTwoWay()),
                    makePort("T", go.Spot.Top, false, true),
                    makePort("L", go.Spot.Left, true, false),
                    makePort("R", go.Spot.Right, true, false),
                    makePort("B", go.Spot.Bottom, true, false)
                ));


            goDiagram.nodeTemplateMap.add("Condition",
                $(go.Node, "Auto", nodeStyle(),
                    $(go.Shape, "Diamond",
                        { fill: "#70706F", stroke: null},
                        new go.Binding("fill","fill").makeTwoWay()),
                    $(go.TextBlock,
                        {   name: "TEXTBLOCK",
                            margin: 5,
                            maxSize: new go.Size(200, NaN),
                            wrap: go.TextBlock.WrapFit,
                            textAlign: "center",
                            editable: true,
                            font: "bold 11pt Helvetica, Arial, sans-serif",
                            stroke: 'whitesmoke'
                        },
                        new go.Binding("text", "text").makeTwoWay()),
                    makePort("T", go.Spot.Top, false, true),
                    makePort("L", go.Spot.Left, true, true),
                    makePort("R", go.Spot.Right, true, true),
                    makePort("B", go.Spot.Bottom, true, false)
                ));

            goDiagram.nodeTemplateMap.add("Compliance",
                $(go.Node, "Auto", nodeStyle(),
                    $(go.Shape, "ManualOperation",
                        { fill: "#009900", stroke: null},
                        new go.Binding("fill","fill").makeTwoWay()),
                    $(go.TextBlock,
                        {   name: "TEXTBLOCK",
                            margin: 5,
                            maxSize: new go.Size(200, NaN),
                            wrap: go.TextBlock.WrapFit,
                            textAlign: "center",
                            editable: true,
                            font: "bold 11pt Helvetica, Arial, sans-serif",
                            stroke: 'whitesmoke'
                        },
                        new go.Binding("text", "text").makeTwoWay()),
                    makePort("T", go.Spot.Top, true, true),
                    makePort("B", go.Spot.Bottom, true, true) //Two ports, only required
                ));

            goDiagram.nodeTemplateMap.add("Comment",
                $(go.Node, "Auto", nodeStyle(),
                    $(go.Shape, "File",
                        { fill: "#EFF28E", stroke: null }),
                    $(go.TextBlock,
                        {   name: "TEXTBLOCK",
                            margin: 5,
                            maxSize: new go.Size(200, NaN),
                            wrap: go.TextBlock.WrapFit,
                            textAlign: "center",
                            editable: true,
                            font: "bold 11pt Helvetica, Arial, sans-serif",
                            stroke: '#454545'
                        },
                        new go.Binding("text", "text").makeTwoWay())
                    // no ports, because no links are allowed to connect with a comment
                ));

            // replace the default Link template in the linkTemplateMap
            goDiagram.linkTemplate =
                $(go.Link,  // the whole link panel
                    {
                        routing: go.Link.Orthogonal,
                        curve: go.Link.JumpOver,
                        corner: 5,
                        toShortLength: 4,
                        relinkableFrom: true,
                        relinkableTo: true,
                        //reshapable: true,
                        mouseEnter: function(e, link) { link.findObject("HIGHLIGHT").stroke = "rgba(30,144,255,0.2)"; },
                        mouseLeave: function(e, link) { link.findObject("HIGHLIGHT").stroke = "transparent"; }
                    },
                    new go.Binding("points").makeTwoWay(),
                            $(go.Shape,  // the highlight shape, normally transparent
      { isPanelMain: true, strokeWidth: 8, stroke: "transparent", name: "HIGHLIGHT" }),
                    $(go.Shape,  // the link path shape
                        { isPanelMain: true, stroke: "gray", strokeWidth: 2, name: "LINK" }, 
                         new go.Binding("stroke", "stroke").makeTwoWay()), 
                    $(go.Shape,  // the arrowhead
                        { toArrow: "standard", stroke: null, fill: "gray" }),
                    $(go.Panel, "Auto",  // the link label, normally not visible
                        { visible: false, name: "LABEL", segmentIndex: 2, segmentFraction: 0.5 },
                        new go.Binding("visible", "visible").makeTwoWay(),
                        $(go.Shape, "RoundedRectangle",  // the label shape
                            { fill: "#F8F8F8", stroke: null }),
                        $(go.TextBlock, "Enter Choice",  // the label
                            {   
                                name: "TEXTBLOCK",
                                textAlign: "center",
                                font: "10pt helvetica, arial, sans-serif",
                                stroke: "#333333",
                                editable: true,
                            },
                            new go.Binding("text", "text").makeTwoWay(),
                            new go.Binding("stroke", "stroke").makeTwoWay())
                    )
                );

            // Make link labels visible if coming out of a "conditional" node.
            // This listener is called by the "LinkDrawn" and "LinkRelinked" DiagramEvents.
            function showLinkLabel(e) {
                var label = e.subject.findObject("LABEL");
                if (label !== null) {
                    label.visible = (e.subject.fromNode.data.category === "Condition");
                }
            }

            // temporary links used by LinkingTool and RelinkingTool are also orthogonal:
            goDiagram.toolManager.linkingTool.temporaryLink.routing = go.Link.Orthogonal;
            goDiagram.toolManager.relinkingTool.temporaryLink.routing = go.Link.Orthogonal;

            load();  // load an initial diagram from some JSON text

            // initialize the Palette that is on the left side of the page
            myPalette =
                $(go.Palette, "myPalette",  // must name or refer to the DIV HTML element
                    {
                        nodeTemplateMap: goDiagram.nodeTemplateMap,  // share the templates used by goDiagram
                        model: new go.GraphLinksModel([  // specify the contents of the Palette
                            { category: "Start", text: "<%= GetLocalResourceObject("gojsStartTagResource1")%>" },
                            { category: "ActionNode", text: "<%=GetLocalResourceObject("gojsActionTagResource1")%>", color: "#1E90FF" },
                            { category: "Condition", text: "<%=GetLocalResourceObject("gojsConditionTagResource1")%>", figure: "Diamond" },
                            { category: "Comment", text: "<%= GetLocalResourceObject("gojsCommentTagResource1")%>", figure: "RoundedRectangle" },
                            { category: "Compliance", text: "<%= GetLocalResourceObject("gojsComplianceResource1")%>", figure: "ManualEntry" },
                            { category: "End", text: "<%= GetLocalResourceObject("gojsEndTagResource1")%>" }
                        ])
                    });
                } // init END


                // Make all ports on a node visible when the mouse is over the node
                function showPorts(node, show) {
                    var diagram = node.diagram;
                    if (!diagram || diagram.isReadOnly || !diagram.allowLink) return;
                    node.ports.each(function (port) {
                        port.stroke = (show ? "white" : null);
                    });
                }


                // Show the diagram's model in JSON format that the user may edit
                function load() {
                    goDiagram.model = go.Model.fromJson((jQuery)("#mySavedModel").val());
                    if (IsDisabled) // thsi doesn't appear to be working with goJS
                    {
                        goDiagram.isDisabled = true;
                    }
                
                    goDiagram.isReadOnly =  IsReadOnly;
                 
                }

If you are using multiple “ports” per node, you need to remember the port id for the ends of each link. That means setting GraphLinksModel.linkFromKeyProperty and GraphLinksModel.linkToKeyProperty.
Read http://gojs.net/latest/intro/ports.html and http://gojs.net/latest/api/symbols/GraphLinksModel.html. And see an example in the Flow Chart sample.

OK, I got that working, remembering the selected port. It is not very intuitive which properties go into the model data and which go into the template. I would never guessed that this would have to be set in the model.

Any ideas on why the ports on my diamond shape show up on the inside of the object?

Thanks,
Bryan

Whether or not you put the port identifiers in the model data depends on what expectations you have in your application. It’s certainly not the case that they should always be saved as properties on the link data object. They might be computed. Or they might be constants in the link template, with different templates using different port identifiers.

However if the user can control which port each end of a link may connect with, it is most common to have the port identifiers saved on the link data object.

Regarding your other problem – don’t the ports appear inside the shape for your other nodes as well?

As it is defined in the “Condition” node template, your node Panel is of type “Auto”, which means all of its immediate elements (other than the main element) will be fitted inside the bounds of the main element, which in this case is a “Diamond” Shape.

If you want to position the ports (or really any elements) partly or complete outside of the main element, use a “Spot” Panel. Set the GraphObject.alignment and GraphObject.alignmentFocus to control where each element is placed relative to the main element. Some of the other samples make use of this node structure – for example: http://gojs.net/latest/samples/dataFlow.html, http://gojs.net/latest/samples/draggableLink.html, http://gojs.net/latest/samples/flowBuilder.html, and the Flow Chart sample itself: http://gojs.net/latest/samples/flowchart.html

Thanks for the reply. The “ports inside” problem is shown as below. You can see how the ports are being drawn inside the model, not at the edges which is prefered. The workflow sample you have does do it correctly. Perhaps you can tell me which property I have set incorrectly, I can’t seem to find it.

It’s all in the structure of the Node’s visual tree.

I think most of the samples do something like:

  Node, "Spot"
    Panel, "Auto"
      Shape, "Diamond"
      TextBlock, "???"
    Shape, "Circle"  // a port
    Shape, "Circle"  // a port
    Shape, "Circle"  // a port
    Shape, "Circle"  // a port

Spent some more time looking at this…all the samples that show the ports correctly, do not use a nodeTemplate for diamond node definition. So basically, the examples don’t help me. The diamond ??? node does behave correctly except for the port locations.

Any ideas on how I can change the nodeTemplate that will fix the port location? The js code I am using is posted above.

Thanks,
Bryan

That’s right – those samples use a node template that has the structure that I showed you in my previous reply.

I highly recommend that you use that visual tree structure in your node template, just as those samples do. You can either create your template from scratch or copy-adapt the one from the FlowChart sample.