Frustrated - Nodes Layout

So this is a follow up to my previous posting regarding Layout Positioning. See: Why Doesn’t This Work?? - Port Positioning at

I have followed up by:

a) modifying and making the layout occur in LayoutChildren().
b) I made positioning relative to the [0] node which is either a GoText or GoImage object.

None of this is working. And I am getting rather weird results. A few comments before I get into the scenarios I am seeing…

  1. I find that doing things in LayoutChildren() is a bit hairy as it seems to be a very recursive function. For instance if I call SetSpotLocation() and as part of the parameter list I use the node (this), then it gets called recursively 2-3 times.

  2. When I use the [0] object for relative setting of the SetSpotLocation(), versus the node, I get crap, nothing even reasonable. When I use the node as in the call, I get varying results, and notice in the debugger that the width and height of the node are constantly changing as I am adding port/labels.

  3. The documentation on all of this is VERY weak and I am finding it frustrating. For instance do you use GoObject.TopCenter or GoObject.MiddleCenter to mean the top of the referenced object? I would assume TopCenter, but your documentation (GoUserGuide pg 46) uses MiddleCenter.

Scenarios:

  1. In scenario 1, I used iterate over all ports each time LayoutChildren is called and call the appropriate SetSpotLocation() routine using the node itself as the obj passed in. In this scenario, I can get North/South to position reasonable correctly to Center, but when I add East, East overlaps center. However, add an West and suddenly East works properly. Contrarily, if I take North (or South) away, the opposite still works. Interestingly as I add cardinal direction (N,S,E,W, SE, SW, NW, NE) the size grows, until with all of them I have to full screen the view to see them - yet they are positioned correctly.

  2. In scenario 2, I am doing the same as scenario 1, except I only SetSpotLocation() with the changedObj passed into the function, and leave the rest alone. This one never works even with pairs of coordinates.

  3. In scenario 3, I iterate over all ports each time LayoutChildren is called, but in the SetSpotLocation() I pass the reference object as the center child node which (currently) is a GoText object, already positioned and centered. In this case, the whole node (which was centered in a view moves to upper-left of the view, and all text is dropped over the same spot.

Below is the relevant code. My organization is on the verge of purchasing licenses for our work, but if I can’t figure out the ju ju black magic involved here, it is gonna put a big crimp in that.

public class DiagramNode : GoNode
{
public DiagramNode()
{
this.Initializing = true;
}

    public Image IconImage
    {
        get
        {
            GoObject obj = this[0];
            if (obj is GoImage)
                return ((GoImage)obj).Image;
            else
                return null;
        }

        set
        {
            GoObject obj = this[0];
            if (obj is GoImage)
                ((GoImage)obj).Image = value;
            else
            {
                GoImage gimg = new GoImage();
                gimg.Image = value;
                Insert(0, gimg);
            }
        }
    }

    public override GoText Label
    {
        get
        {
            GoObject obj = this[0];
            if (obj is GoGeneralNodePort)
            {
                return (GoText)((GoGeneralNodePort)obj).PortObject;
            }
            else
                return null;
        }
        set
        {
            AddLabelAt(DiagramConstants.CardinalDirections.Center, value.Text);
        }
    }

    public void AddLinkAt(DiagramConstants.CardinalDirections direction, DiagramLink link)
    {
    }

    public void ConnectNodeAt(DiagramConstants.CardinalDirections direction, DiagramNode node)
    {
    }

    public void AddLabelAt(DiagramConstants.CardinalDirections direction, string text)
    {
        GoGeneralNodePort port = new GoGeneralNodePort();
        port.ToSpot = (int) direction;
        port.FromSpot = (int) direction;
        port.Style = GoPortStyle.Object;
        GoText gtext = new GoText();
        gtext.Text = text;
        port.Editable = false;
        port.PortObject = gtext;
        if (direction == DiagramConstants.CardinalDirections.Center)
            Insert(0, port);
        else
            Add(port);
    }

    public override void LayoutChildren(GoObject childchanged)
    {
        if (!this.Initializing)
        {
            //base.LayoutChildren(childchanged);
            GoObject center = null;
            if (this[0] is GoPort)
            {
                center = ((GoGeneralNodePort)this[0]).PortObject;
            }
            else if (this[0] is GoImage)
            {
                center = this[0];
            }
            else
                center = new GoRectangle();
            foreach (GoPort p in this.Ports)
            {
                int direction = p.ToSpot;
                switch (direction)
                {
                    case (int)DiagramConstants.CardinalDirections.Center:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.Center, this, (int)DiagramConstants.CardinalDirections.Center);
                        break;
                    case (int)DiagramConstants.CardinalDirections.East:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.West, center, (int)DiagramConstants.CardinalDirections.East);
                        break;
                    case (int)DiagramConstants.CardinalDirections.North:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.South, center, (int)DiagramConstants.CardinalDirections.North);
                        break;
                    case (int)DiagramConstants.CardinalDirections.NorthEast:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.SouthWest, center, (int)DiagramConstants.CardinalDirections.NorthEast);
                        break;
                    case (int)DiagramConstants.CardinalDirections.NorthWest:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.SouthEast, center, (int)DiagramConstants.CardinalDirections.NorthWest);
                        break;
                    case (int)DiagramConstants.CardinalDirections.South:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.North, center, (int)DiagramConstants.CardinalDirections.South);
                        break;
                    case (int)DiagramConstants.CardinalDirections.SouthEast:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.NorthWest, center, (int)DiagramConstants.CardinalDirections.SouthEast);
                        break;
                    case (int)DiagramConstants.CardinalDirections.SouthWest:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.NorthEast, center, (int)DiagramConstants.CardinalDirections.SouthWest);
                        break;
                    case (int)DiagramConstants.CardinalDirections.West:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.East, center, (int)DiagramConstants.CardinalDirections.West);
                        break;
                }
            }
            /*if (childchanged is GoGeneralNodePort)
            {
                GoGeneralNodePort p = (GoGeneralNodePort)childchanged;
                int direction = p.ToSpot;
                switch (direction)
                {
                    case (int)DiagramConstants.CardinalDirections.Center:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.Center, this, (int)DiagramConstants.CardinalDirections.Center);
                        break;
                    case (int)DiagramConstants.CardinalDirections.East:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.West, this, (int)DiagramConstants.CardinalDirections.East, this.Width * 2, 0F);
                        break;
                    case (int)DiagramConstants.CardinalDirections.North:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.South, this, (int)DiagramConstants.CardinalDirections.North, 0F, -5F);
                        break;
                    case (int)DiagramConstants.CardinalDirections.NorthEast:
                        p.SetSpotLocation((int)direction, this, (int)direction);
                        break;
                    case (int)DiagramConstants.CardinalDirections.NorthWest:
                        p.SetSpotLocation((int)direction, this, (int)direction);
                        break;
                    case (int)DiagramConstants.CardinalDirections.South:
                        p.SetSpotLocation((int)DiagramConstants.CardinalDirections.North, this, (int)DiagramConstants.CardinalDirections.South, 0F, 5F);
                        break;
                    case (int)DiagramConstants.CardinalDirections.SouthEast:
                        p.SetSpotLocation((int)direction, this, (int)direction);
                        break;
                    case (int)DiagramConstants.CardinalDirections.SouthWest:
                        p.SetSpotLocation((int)direction, this, (int)direction);
                        break;
                    case (int)DiagramConstants.CardinalDirections.West:
                        p.SetSpotLocation((int)direction, this, (int)direction);
                        break;
                }
            }*/
        }
    }
}

public static DiagramNode CreateTextOnlyNode(string text)
{
DiagramNode node = new DiagramNode();
//GoText textComp = new GoText();
//textComp.Text = text;
//node.Label = textComp;
node.Editable = false;
node.Width = 200;
node.Height = 200;

        string text2 = text + "port";
        node.AddLabelAt(DiagramConstants.CardinalDirections.Center, "Center");
        node.AddLabelAt(DiagramConstants.CardinalDirections.North, "North");
        node.AddLabelAt(DiagramConstants.CardinalDirections.East, "East");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.South, "South");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.West, "West");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.NorthEast, "NorthEast");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.SouthEast, "SouthEast");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.SouthWest, "SouthWest");
        //node.AddLabelAt(DiagramConstants.CardinalDirections.NorthWest, "NorthWest");
        return node;
    }</i>

Scenario 3 Image

Scenario 1&2 Images

[code] [Serializable]
public class LukeNode : GoIconicNode {
public LukeNode() { }

public GoPort AddLabelAt(int spot, String text) {
GoPort p = new GoPort();
// port’s appearance is given by a GoText object, not one of the usual port styles
p.Style = GoPortStyle.Object;
GoText t = new GoText();
t.FontSize = 6;
t.Text = text;
p.PortObject = t; // don’t add this GoText object to the node!

p.Size = t.Size; // make sure the port is just as big as the text
p.FromSpot = spot; // links coming out of this port connect at the spot point
p.ToSpot = spot; // ibid for links going into this port
p.UserFlags = spot; // remember the spot for LayoutChildren's sake
Add(p);
return p;
}

public override void LayoutChildren(GoObject childchanged) {
base.LayoutChildren(childchanged);
// if there's an Icon, position each port at the spot given by the port's UserFlags property,
// which must be a GoObject spot
if (this.Icon == null) return;
foreach (GoObject obj in this) {
GoPort p = obj as GoPort;
if (p == null) continue;
p.SetSpotLocation(SpotOpposite(p.UserFlags), this.Icon, p.UserFlags);
}
}
}[/code]

Example usage:

[code] LukeNode n = new LukeNode();
GoText lab = new GoText();
lab.Selectable = false;
lab.Multiline = true;
lab.Editable = true;
lab.Bold = true;
lab.Bordered = true;
lab.Text = "ICON";
n.Icon = lab;
n.AddLabelAt(GoObject.MiddleTop, "MT");
n.AddLabelAt(GoObject.TopLeft, "TL");
n.AddLabelAt(GoObject.BottomRight, "BR");
doc.Add(n);
n.AddLabelAt(GoObject.MiddleLeft, "ML");
n.AddLabelAt(GoObject.MiddleBottom, "MB");[/code]

Result, after copying and editing the text to make it bigger, and then drawing some orthogonal links between the nodes:



Regarding your particular questions:

(1 & 2) Yes, and that's why the documentation for LayoutChildren says:
[quote]Implementations of this method probably should not refer, directly or indirectly, to this group's GoObject.Bounds property. Instead, you should just position and size the children based on the bounds of the children (not this group's bounds), and let this group's bounds be determined by the union of the bounds of the children.[/quote]

(3) I can't find "MiddleCenter" anywhere in the User Guide. GoObject.MiddleCenter is a synonym for GoObject.Middle.

Oh, and if you want an image instead of text as the Icon:

LukeNode n = new LukeNode(); n.Initialize(null, "star.gif", null); n.Icon.Size = new SizeF(40, 40); n.Port = null; // get rid of default Port in middle of Icon, created by Initialize n.AddLabelAt(GoObject.MiddleTop, "MT"); n.AddLabelAt(GoObject.TopLeft, "TL"); n.AddLabelAt(GoObject.BottomRight, "BR"); doc.Add(n); n.AddLabelAt(GoObject.MiddleLeft, "ML"); n.AddLabelAt(GoObject.MiddleBottom, "MB");

This results in:

A few questions and clarifications:

  1. Sorry it was MiddleTop and not MiddleCenter. But the user guide and the code doc are very unclear on these directions.

  2. Do I have to use n.Initialize(null, “star.gif”, null) to get an image?

  3. Why did it not work, when I based the positioning off of a center “port” object versus the node object? That got even uglier.

  4. Why do you give objects (n.Icon.Size) when i one of your responses you said that sizing varied based on the actual objects contained?

  5. Why does this work for GoIconicNode and not GoNode subclassing?

  6. Is there some specific reason as to why you AddLabel 3 times then add to doc, then add more? Want to make sure I am not missing some black magic here.

  1. Sorry about that. If you could provide alternate wording that would help people like you, I would appreciate it.

  2. No, but it’s a convenient way to give the GoIconicNode an Icon (a GoImage in this case) and a Label (none in this case) and a Port. Otherwise you would have to create and assign them separately.

  3. It’s probably because as you were positioning the ports, the Bounds of the node kept changing. Also, now that I look at your code, I notice that your implementation of LayoutChildren would sometimes create a new GoRectangle, which by default is positioned at (0,0), and then position each of the ports relative to that uninitialized GoRectangle.

  4. That’s just making the GoImage the size I wanted. If you don’t set it, the GoImage will be sized according to the natural size of the Image that it is showing. Furthermore GoImage (and GoText) are not GoGroups – so that “rule” does not apply about changing the size has different results depending on the child objects and the implementation of LayoutChildren.

  5. GoIconicNode is just a GoNode with some predefined parts and behavior. There’s no magic there – all of the predefined GoNode subclasses can be implemented by anyone using only public and protected GoNode/GoGroup/GoObject members. I just chose to use GoIconicNode to save some implementation work.

As you see with the PinNode example, you can implement such nodes by inheriting from GoNode. In this case I thought it would be less work to implement, clearer to understand, and more flexible/functional by inheriting from GoIconicNode. Do you agree that the class I posted above is reasonably clear and concise? Does it appear that you can easily change or extend the behavior, for example by changing the call the SetSpotLocation to handle particular spots in different manners to increase the spacing between the port and the Icon? Can you imagine what you would need to do if you wanted to support multiple ports with the same spot value?

  1. I just wanted to show that you could call AddLabelAt at any time, before or after adding the node to the document.

So it still is not working properly… Screenshots below.

In response to your questions (btw, thanks for you help):

  1. You could use alternate cardinal terms like North, South, East, West… I know we live in a geographically challenged society, but surely people can figure out the “north” of a node. I guess what confused me in your naming was the difference between Middles and Tops/Bottoms. The combos weren’t clear to me.

  2. The GoRectangle piece was added later to try to get it to work based on some of the demo code I saw. So it was not in my original “test”. Is the reason why mine failed was I was anchoring (on Center) everything on a port which is a GoGroup object and changed bounds? Is that what you are saying?

  1. The cartographer in me didn’t like the idea of associating “north” with “top” or “up”. Maps often don’t have north pointing up.
I realize that even "top" and "up" can be misleading, but I assumed that most programmers will be sitting (or slouching) upright, not upside-down or in a zero-gravity environment.
3. Yes, when you position a child relative to the whole node, and such movement causes the whole node's Bounds to change, then you'll get the behavior you saw.
It would actually work if you made sure you got the node's Bounds first, in a local variable, and only worked off that RectangleF.
If you really want to do absolute positioning/sizing in a fixed-size node and you want to be able to refer to the node.Bounds at any time during the layout process, you could add to your node an inVisible GoRectangle of the size you want. You'll just need to make sure that no child extends beyond the Bounds of this GoRectangle, because that would change the node's Bounds again. But this is an very uncommon layout strategy.
Also, as I mentioned before, if you don't need to worry about any child being resized or moved, you don't need an override of LayoutChildren. Just do absolute positioning for each of the child objects that you add to the node. Some of the simpler example nodes do this, if I recall correctly.
  1. Top is not confusing, but TopCenter and MiddleTop are. So is Middle and MiddleCenter. Quite frankly you could resolve all of this with a better description than: “This represents a point in the object’s bounds.”, for TopCenter.

did you see the pictures of the results based on the code samples you provided? It still does not appear to be positioning correctly. Do I have a wrong version or something?

Below is a copy of the coordinate mappings. it seems that the “left side” (west, sw, nw) are what are skewed to the center. Right side seems to be fine…

        North = GoObject.TopCenter,
        NorthEast = GoObject.TopRight,
        East = GoObject.MiddleRight,
        SouthEast = GoObject.BottomRight,
        South = GoObject.BottomCenter,
        SouthWest = GoObject.BottomLeft,
        West = GoObject.MiddleLeft,
        NorthWest = GoObject.TopLeft,
        Center = GoObject.Middle
I think I see a bug in my original definition of LukeNode.
Add the following line when setting the GoPort.PortObject:
[code] p.Size = t.Size; // make sure the port is just as big as the text[/code]
I didn't see that problem before because the text strings were too short for it to matter.
I have already updated the code above.

Here is the code:

public class DiagramNode : GoIconicNode
{
public DiagramNode()
{
}

    public Image IconImage
    {
        get
        {
            GoImage obj = this.Icon as GoImage;
            if (obj == null) return null;
            return obj.Image;
                
        }

        set
        {
            GoObject obj = this.Icon;
            if (obj is GoImage)
                ((GoImage)obj).Image = value;
            else
            {
                GoImage gimg = new GoImage();
                gimg.Image = value;
                this.Icon = gimg;
            }
            this.Icon.Size = value.Size;
        }
    }

    public override GoText Label
    {
        get
        {
            GoObject obj = this.Icon;
            return obj as GoText;
        }
        set
        {
            this.Icon = value;
        }
    }

    public void AddDestinationLinkAt(DiagramConstants.CardinalDirections direction, DiagramLink link)
    {
        GoPort port = GetPortAt(direction);
        if (port == null)
        {
            port = CreatePort(direction);
        }
        link.FromPort = port;
        port.AddDestinationLink(link);
    }

    public void AddSourceLinkAt(DiagramConstants.CardinalDirections direction, DiagramLink link)
    {
        GoPort port = GetPortAt(direction);
        if (port == null)
        {
            port = CreatePort(direction);
        }
        link.ToPort = port;
        port.AddSourceLink(link);
    }

    public void AddLabelAt(DiagramConstants.CardinalDirections direction, string text)
    {
        GoPort port = GetPortAt(direction);
        if (port == null)
        {
            port = CreatePort(direction);
        }
        port.Style = GoPortStyle.Object;
        GoText gtext = new GoText();
        gtext.FontSize = DiagramConstants.NORMAL_FONT_SIZE;
        gtext.Text = text;
        port.PortObject = gtext;
        Add(port);
    }

    public override void LayoutChildren(GoObject childchanged)
    {
        base.LayoutChildren(childchanged);
        // if there's an Icon, position each port at the spot given by the port's UserFlags property,
        // which must be a GoObject spot
        if (this.Icon == null) return;
        foreach (GoObject obj in this)
        {
            GoPort p = obj as GoPort;
            if (p == null) continue;
            p.SetSpotLocation(SpotOpposite(p.UserFlags), this.Icon, p.UserFlags);
        }
    }

    private GoPort GetPortAt(DiagramConstants.CardinalDirections direction)
    {
        foreach (GoObject obj in this)
        {
            GoPort p = obj as GoPort;
            if(p != null && p.UserFlags == (int)direction)
                return p;
        }
        return null;
    }

    private GoPort CreatePort(DiagramConstants.CardinalDirections direction)
    {
        GoPort port = new GoPort();
        port.ToSpot = (int)direction;
        port.FromSpot = (int)direction;
        port.UserFlags = (int)direction;
        port.Editable = false;
        return port;
    }
}


        DiagramNode node = new DiagramNode();

        node.Editable = false;

        GoText gtext = new GoText();
        gtext.Text = "Center";
        node.Label = gtext;
        node.AddLabelAt(DiagramConstants.CardinalDirections.North, "North");
        node.AddLabelAt(DiagramConstants.CardinalDirections.East, "East");
        node.AddLabelAt(DiagramConstants.CardinalDirections.South, "South");
        node.AddLabelAt(DiagramConstants.CardinalDirections.West, "West");
        node.AddLabelAt(DiagramConstants.CardinalDirections.NorthEast, "NorthEast");
        node.AddLabelAt(DiagramConstants.CardinalDirections.SouthEast, "SouthEast");
        node.AddLabelAt(DiagramConstants.CardinalDirections.SouthWest, "SouthWest");
        node.AddLabelAt(DiagramConstants.CardinalDirections.NorthWest, "NorthWest");
        return node;

Our posts may have crossed – please note my previous post.

Thanks… Looks much better, almost there. “West” is butted directly up against “Center”, while “East” looks like it has a bit of space between it and “Center”.