Labels on edges: a solution using high-school trigonometry

Oct 15, 2009 at 6:30 PM
Edited Oct 16, 2009 at 7:29 AM

One of the missing things in Graph# is the ability to label edges. Pure edge labelling is a difficult problem. Here is an approach which. though not perfect, may be a starting point for making your own edge labels.

The first thing to define is a graph with tagged edges. The tag of an edge is an object: this object will be shown, as dictated by WPF (a datatemplate, or the ToString() representation. This makes it compliant with MVVM:

    public class TaggedGraphLayout : GraphLayout<object, TaggedEdge<object, object>, IBidirectionalGraph<object, TaggedEdge<object, object>>>
    {
    }

With this definition, you can construct a labelled edge graph like this:

            var g = new BidirectionalGraph<object, TaggedEdge<object, object>>();

            //add the vertices to the graph
            Person[] vertices = new Person[5];
            for (int i = 0; i < 5; i++)
            {
                vertices[i] = new Person { FirstName = String.Format("FirstName{0}", i), LastName = String.Format("LastName{0}", i) };
                g.AddVertex(vertices[i]);
            }

            //add some edges to the graph
            g.AddEdge(new TaggedEdge<object, object>(vertices[0], vertices[1], "Tag1"));
            g.AddEdge(new TaggedEdge<object, object>(vertices[1], vertices[2], "Tag2"));
            g.AddEdge(new TaggedEdge<object, object>(vertices[2], vertices[3], "Tag3"));
            g.AddEdge(new TaggedEdge<object, object>(vertices[3], vertices[1], "Tag4"));
            g.AddEdge(new TaggedEdge<object, object>(vertices[1], vertices[4], "Tag5"));

 

 We're done with the easy part. Now for the hard part: what you need to do is to define a new control template for EdgeControl. This control template will contain a canvas with 2 elements: a Path figure with the actual edge (brutally and shamelessly copied from EdgeControl's template), and a labelled edge control bound to the edge tag:

  

        <Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />
        <Style TargetType="{x:Type Controls:EdgeControl}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type Controls:EdgeControl}">
                        <Canvas>
                            <Path Stroke="{TemplateBinding Foreground}"
                          StrokeThickness="2"
                          MinWidth="1"
                          MinHeight="1"
                          x:Name="edgePath">
                                <Path.Data>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <MultiBinding Converter="{StaticResource routeToPathConverter}">
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"t;/span>
                                                 Path="Source.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="RoutePoints" />
                                            </MultiBinding>
                                        </PathGeometry.Figures>
                                    </PathGeometry>
                                </Path.Data>
                            </Path>
                            <local:EdgeLabelControl Content="{Binding Edge.Tag, RelativeSource={RelativeSource TemplatedParent}}" />
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

Put this template somewhere, where it can be seen from the visual graph. Don't forget to set the "local:" namespace to the correct assembly where the LabelledEdgeControl is defined. Check the other namespace prefixes as wel. I'm sure you know the drill.

And for the definition of that control, use the following (it contains the high school trig we all love):

    public class EdgeLabelControl : ContentControl
    {
        public EdgeLabelControl()
        {
            LayoutUpdated += EdgeLabelControl_LayoutUpdated;
        }

        private EdgeControl GetEdgeControl(DependencyObject parent)
        {
            while (parent != null)
                if (parent is EdgeControl)
                    return (EdgeControl)parent;
                else
                    parent = VisualTreeHelper.GetParent(parent);
            return null;
        }

        private static double GetAngleBetweenPoints(Point point1, Point point2)
        {
            return Math.Atan2(point1.Y - point2.Y, point2.X - point1.X);
        }

        private static double GetDistanceBetweenPoints(Point point1, Point point2)
        {
            return Math.Sqrt(Math.Pow(point2.X - point1.X, 2) + Math.Pow(point2.Y - point1.Y, 2));
        }

        private static double GetLabelDistance(double edgeLength)
        {
            return edgeLength / 2;  // set the label halfway the length of the edge
        }

        private void EdgeLabelControl_LayoutUpdated(object sender, EventArgs e)
        {
            if (!IsLoaded)
                return;
            var edgeControl = GetEdgeControl(VisualParent);
            if (edgeControl == null)
                return;
            var source = edgeControl.Source;
            var p1 = ot;>new Point(GraphCanvas.GetX(source), GraphCanvas.GetY(source));
            var target = edgeControl.Target;
            var p2 = new Point(GraphCanvas.GetX(target), GraphCanvas.GetY(target));

            double edgeLength;
            var routePoints = edgeControl.RoutePoints;
            if (routePoints == null)
                // the edge is a single segment (p1,p2)
                edgeLength = GetLabelDistance(GetDistanceBetweenPoints(p1, p2));
            else
            {
                // the edge has one or more segments
                // compute the total length of all the segments
                edgeLength = 0;
                for (int i = 0; i <= routePoints.Length; ++i)
                    if (i == 0)
                        edgeLength += GetDistanceBetweenPoints(p1, routePoints[0]);
                    else if (i == routePoints.Length)
                        edgeLength += GetDistanceBetweenPoints(routePoints[routePoints.Length - 1], p2);
                    else
                        edgeLength += GetDistanceBetweenPoints(routePoints[i - 1], routePoints[i]);
                // find the line segment where the half distance is located
                edgeLength = GetLabelDistance(edgeLength);
                Point newp1 = p1;
                Point newp2 = p2;
                for (int i = 0; i <= routePoints.Length; ++i)
                {
                    double lengthOfSegment;
                    if (i == 0)
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = p1, newp2 = routePoints[0]);
                    else if (i == routePoints.Length)
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = routePoints[routePoints.Length - 1], newp2 = p2);
                    else
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = routePoints[i - 1], newp2 = routePoints[i]);
                    if (lengthOfSegment >= edgeLength)
                        break;
                    edgeLength -= lengthOfSegment;
                }
                // redefine our edge points
                p1 = newp1;
                p2 = newp2;
            }
            // align the point so that it  passes through the center of the label content
            var p = p1;
            var desiredSize = DesiredSize;
            p.Offset(-desiredSize.Width / 2, -desiredSize.Height / 2);

            // move it "edgLength" on the segment
            var angleBetweenPoints = GetAngleBetweenPoints(p1, p2);
            p.Offset(edgeLength * Math.Cos(angleBetweenPoints), -edgeLength * Math.Sin(angleBetweenPoints));
            Arrange(new Rect(p, desiredSize));
        }
    }

 

The placement of the label is always at halfway the edge length. If the edge consists of multilple segments, the label is aligned in the correct segment. It's a stupid heuristic, but it kinda works well.

If you are adventurous, you can use a rotation matrix to rotate the lable at an angle corresponding to the angle of the edge segment. I didn't do this, because it gave me a pain in the neck (literally!) reading the labels. You have been warned.

That's all there is to it. The placement is purely visual, and the label itself does not participate in the edge route computation (when present: I'm talking about Sugiyama, here, since nontrivial routing is currently missing from all other layout algorithms). But it's a start: the esteemed author of Graph# will surely improve on the method.

Have fun.

Vincent

Nov 8, 2009 at 6:05 PM

Hi.

Unfortunetly your solution does not worked for me. Besides the label does not appear on the graph I obtain an error:

Error    1    A 'MultiBinding' cannot be used within a 'PathFigureCollection' collection. A 'MultiBinding' can only be set on a DependencyProperty of a DependencyObject.    C:\Documents and Settings\Daniela\Meus documentos\Visual Studio 2008\Projects\GraphSharpTest\GraphSharpTest\Window1.xaml   

 

Can you help me?

Do you have any example working?

 

thanks

Mar 26, 2010 at 7:53 AM

Hey wdb,

Thanks for the great solution! I have used it and it works great! However I have one question:

  1. Using your solution, how would I catch a MouseDown event of an edge? (I have tried getting the Parent of the controls but I cant get to the Taggededge itself.). I want to be able to delete the edge and thought I'd ask here as you are the author of this piece of code.

Sincerely.

rootme

Mar 26, 2010 at 8:10 AM
Edited Mar 26, 2010 at 8:11 AM

Hello rootme,

I'm glad to hear the solution works for you.

Your question about intercepting the MouseDown event is interesting. I don't have the project at hand, so I can only tell you what I would do and provide some pointers.

What you *really* want to do is not catch MouseDown, but delete the edge. You have a reference to your tagged edge in the EdgeLabelControl: it's the Content property (because it's a ContentControl). So I would define an interface, say:

interface IEdgeCommand
{
    void DoOperation();
}

Implement this interface on your TaggedEdge:

class MyTaggedEdge: TaggedEdge<whatever,whatever>, IEdgeCommand
{
    ... (make sure you have everything you need to perform whatever you want here)...
}

...and finally handle the MouseDown event in the EdgeLabelControl, where I would cast the Content property to IEdgeCommand, and call the function.

Once you're in your tagged edge, you're home free.

That's, on the top of my head, how I would do it. Let me know what develops <g>.

vvdb

 

 

Mar 29, 2010 at 9:32 AM

Perfect!!!

Thank you very much!

Regards,

rootme

 

Mar 31, 2010 at 3:55 PM

Thanks for the solution, I find it very useful!

Thanks vvdb and rootme.

May 24, 2010 at 11:23 AM
Hey wdb, Its me again. Is it possible to create different "styles" for the tagged edges on one graph? I would like different edges e.g: 1. Red Line 2. Stroked Line (e.g. - - - - - - - - - - - - ) 3. Bold Line 4. etc. All on one graph. Is this possible at all? Can you propose how I would go about achieving this? Thank you rootme
Jun 26, 2010 at 11:54 AM
Edited Jun 26, 2010 at 11:56 AM
Hi, i'm a little new at this and having some trouble using this code.

First of all i found these odd phrases in the code: ;/span> (in the xmal) ot;> (int the C#) I guest it's just typos so i deleted them.

But the real problem is that I don't really know where to put the xaml code. If you could provide a working example it would be great. If not - that's my xaml code which is drawing the graph, where should i put the other xaml code? should i open a new wpf file? I tried that and it won't build because of the first line with the x:key not being inside IDictionary tag. And even if it is built, how should i initialize the wpf component?

thanks

<Window x:Class="MyProject.Visualizer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Local="clr-namespace:MyProject"
xmlns:GraphSharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls"
xmlns:zoom="clr-namespace:WPFExtensions.Controls;assembly=WPFExtensions"
Title="Graph"
Height="900" Width="900"
x:Name="root">
<Grid>
<zoom:ZoomControl>
<GraphSharp:GraphLayout x:Name="graphLayout"
Graph="{Binding ElementName=root,Path=GraphToVisualize}"
LayoutAlgorithmType="FR"
OverlapRemovalAlgorithmType="FSA"
HighlightAlgorithmType="Simple"
/>
</zoom:ZoomControl>
</Grid>
</Window>
Jul 14, 2010 at 8:56 AM

It is possible to create different styles for the tagged edges. Although I never did it, I would suggest looking at the Stroke, StrokeThickness, StrokeDashArray and StrokeDashCap in the Path tag

Jul 14, 2010 at 11:15 AM
Edited Jul 14, 2010 at 11:18 AM

vvdb can you please guide me where to keep code...

I have created new class and place EdgeLabelControl class code in it and replaced your given xaml code for edge style in generic.xaml

I created following class in Graphsharp.Controls project

 public class TaggedGraphLayout : GraphLayout, IBidirectionalGraph>>
    {
    }
JUST WANTED TO MAKE SURE IF I AM ON RIGHT TRACK OR NOT..
but WHEN I PUT THIS CODE IN LayoutAnalyzerViewModel CLASS 
       
     var g = new BidirectionalGraph>();

            //add the vertices to the graph
            Person[] vertices = new Person[5];
            for (int i = 0; i < 5; i++)
            {
                vertices[i] = new Person { FirstName = String.Format("FirstName{0}", i), LastName = String.Format("LastName{0}", i) };
                g.AddVertex(vertices[i]);
            }

            //add some edges to the graph
            g.AddEdge(new TaggedEdge(vertices[0], vertices[1], "Tag1"));
            g.AddEdge(new TaggedEdge(vertices[1], vertices[2], "Tag2"));
            g.AddEdge(new TaggedEdge(vertices[2], vertices[3], "Tag3"));
            g.AddEdge(new TaggedEdge(vertices[3], vertices[1], "Tag4"));
            g.AddEdge(new TaggedEdge(vertices[1], vertices[4], "Tag5"));

 SO FINAL SHAPE OF CLASS IS .... 

 

 

public partial class LayoutAnalyzerViewModel
	{
		partial void CreateSampleGraphs()
		{
            //#region SimpleTree

            //var graph = new PocGraph();

            //for (int i = 0; i < 15; i++)
            //{
            //    var v = new PocVertex(i.ToString());
            //    graph.AddVertex(v);
            //}

            //graph.AddEdge(new PocEdge("0to1 node", graph.Vertices.ElementAt(0), graph.Vertices.ElementAt(1)));
            //graph.AddEdge(new PocEdge("1to2 node", graph.Vertices.ElementAt(1), graph.Vertices.ElementAt(2)));
            //graph.AddEdge(new PocEdge("2to3 node", graph.Vertices.ElementAt(2), graph.Vertices.ElementAt(3)));
            //graph.AddEdge(new PocEdge("2to4 node", graph.Vertices.ElementAt(2), graph.Vertices.ElementAt(4)));
            //graph.AddEdge(new PocEdge("0to5 node", graph.Vertices.ElementAt(0), graph.Vertices.ElementAt(5)));
            //graph.AddEdge(new PocEdge("1to7 node", graph.Vertices.ElementAt(1), graph.Vertices.ElementAt(7)));
            //graph.AddEdge(new PocEdge("4to6 node", graph.Vertices.ElementAt(4), graph.Vertices.ElementAt(6)));
            //graph.AddEdge(new PocEdge("10to11 node", graph.Vertices.ElementAt(5), graph.Vertices.ElementAt(8)));
            //graph.AddEdge(new PocEdge("11to12 node", graph.Vertices.ElementAt(6), graph.Vertices.ElementAt(9)));
            //graph.AddEdge(new PocEdge("12to13 node", graph.Vertices.ElementAt(7), graph.Vertices.ElementAt(10)));
            //graph.AddEdge(new PocEdge("12to14 node", graph.Vertices.ElementAt(8), graph.Vertices.ElementAt(11)));
            //graph.AddEdge(new PocEdge("10to15 node", graph.Vertices.ElementAt(9), graph.Vertices.ElementAt(12)));
            //graph.AddEdge(new PocEdge("11to17 node", graph.Vertices.ElementAt(10), graph.Vertices.ElementAt(13)));
            //graph.AddEdge(new PocEdge("14to16 node", graph.Vertices.ElementAt(11), graph.Vertices.ElementAt(14)));

            //GraphModels.Add(new GraphModel("Fa", graph));

            //#endregion

            #region SimpleTree

            var g = new BidirectionalGraph>();

            //add the vertices to the graph
            Person[] vertices = new Person[5];
            for (int i = 0; i < 5; i++)
            {
                vertices[i] = new Person(i.ToString());
                g.AddVertex(vertices[i]);
            }

            //add some edges to the graph
            g.AddEdge(new TaggedEdge(vertices[0], vertices[1], "Tag1"));
            g.AddEdge(new TaggedEdge(vertices[1], vertices[2], "Tag2"));
            g.AddEdge(new TaggedEdge(vertices[2], vertices[3], "Tag3"));
            g.AddEdge(new TaggedEdge(vertices[3], vertices[1], "Tag4"));
            g.AddEdge(new TaggedEdge(vertices[1], vertices[4], "Tag5"));

            GraphModels.Add(new GraphModel("Fa", g));

            #endregion
		}

        public class Person
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
	}

 

AS SAMPLE APPLICATION HAS POCGRAPH CLASS SO I EXTEND THAT TOO

 

 

public class GraphModel
	{
		public string Name { get; private set; }
		public PocGraph Graph { get; private set; }
        public BidirectionalGraph> BiGraph { get; private set; }

		public GraphModel(string name, PocGraph graph)
		{
			Name = name;
			Graph = graph;
		}
        public GraphModel(string name, BidirectionalGraph> graph)
        {
            Name = name;
            BiGraph = graph;
        }
	}

 

 

BUT NOT GETTING ANY VERTICES OR EDGES... PLEASE CORRECT ME WHERE AM WRONG.. 

 

 

IF Y OU CAN SEND ME SAMPLE CODE AT ADNANAMAN@GMAIL.COM, I WOULD BE VERY GRATEFUL.

Mar 6, 2011 at 3:16 PM

Hello. Can somebody link zip'ed solution here? It will be very helpfull to understand (debug) it.
greetings :]

Apr 21, 2011 at 8:30 PM

Thanks vvdb for the great solution!  After much WPF agonizing, this was the perfect answer to my problems.  I just thought I'd contribute my small embellishment: at first my edge labels were hard to see because they intersect with the edge itself.  To move them slightly to the side, just replace this line:

 

p.Offset(edgeLength * Math.Cos(angleBetweenPoints), -edgeLength * Math.Sin(angleBetweenPoints));


with these five lines:


public float x = 12.5f, y = 12.5f;
double sin = Math.Sin(angleBetweenPoints);
double cos = Math.Cos(angleBetweenPoints);
double sign = sin * cos / Math.Abs(sin * cos);
p.Offset(x * sin * sign + edgeLength * cos, y * cos * sign - edgeLength * sin);
Sep 7, 2011 at 12:35 PM

Hello,

I've been trying to use this solution in my graph demo. Though I cannot find out why I get the following error:

"Object reference not set to an instance of an object."

Here is my code:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:GraphSharpDemo"
                    xmlns:graphsharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls" 
	xmlns:Converters="clr-namespace:GraphSharp.Converters;assembly=GraphSharp.Controls"
	xmlns:WPFExtensions_AttachedBehaviours="clr-namespace:WPFExtensions.AttachedBehaviours;assembly=WPFExtensions">

    <Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />

    <Style TargetType="{x:Type graphsharp:EdgeControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type graphsharp:EdgeControl}">
                    <Canvas>
                        <Path Stroke="{TemplateBinding Foreground}"
                          StrokeThickness="2"
                          MinWidth="1"
                          MinHeight="1"
                          x:Name="edgePath">
                            <Path.Data>
                                <PathGeometry>
                                    <PathGeometry.Figures>
                                        <MultiBinding Converter="{StaticResource routeToPathConverter}">
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.X)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.Y)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualWidth" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                Path="Source.ActualHeight" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.X)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.Y)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualWidth" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualHeight" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="RoutePoints" />
                                        </MultiBinding>
                                     </PathGeometry.Figures>
                                </PathGeometry>
                            </Path.Data>
                        </Path>
                        <local:EdgeLabelControl Content="{Binding Edge.Tag, RelativeSource={RelativeSource TemplatedParent}}" />
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Does anybody have an idea what the problem is? 
Thank you.
Sep 7, 2011 at 12:35 PM
Edited Sep 7, 2011 at 12:40 PM

Hello,

I've been trying to use this solution in my graph demo. Though I cannot find out why I get the following error:

"Object reference not set to an instance of an object."

Here is my code:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:GraphSharpDemo"
                    xmlns:graphsharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls" 
	xmlns:Converters="clr-namespace:GraphSharp.Converters;assembly=GraphSharp.Controls"
	xmlns:WPFExtensions_AttachedBehaviours="clr-namespace:WPFExtensions.AttachedBehaviours;assembly=WPFExtensions">

    <Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />

    <Style TargetType="{x:Type graphsharp:EdgeControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type graphsharp:EdgeControl}">
                    <Canvas>
                        <Path Stroke="{TemplateBinding Foreground}"
                          StrokeThickness="2"
                          MinWidth="1"
                          MinHeight="1"
                          x:Name="edgePath">
                            <Path.Data>
                                <PathGeometry>
                                    <PathGeometry.Figures>
                                        <MultiBinding Converter="{StaticResource routeToPathConverter}">
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.X)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(Controls:GraphCanvas.Y)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualWidth" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                Path="Source.ActualHeight" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.X)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(Controls:GraphCanvas.Y)" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualWidth" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualHeight" />
                                            <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="RoutePoints" />
                                        </MultiBinding>
                                     </PathGeometry.Figures>
                                </PathGeometry>
                            </Path.Data>
                        </Path>
                        <local:EdgeLabelControl Content="{Binding Edge.Tag, RelativeSource={RelativeSource TemplatedParent}}" />
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Does anybody have an idea what the problem is?

Thank you.

EDIT: The real example ends with the </ResourceDictionary> tag. I forgot to insert it into this snippet.

Oct 25, 2011 at 3:37 PM

Hello,

Can somebody give me example that works on email: davidart@tut.by. Thanks...

Oct 26, 2011 at 3:43 PM

Hello,

I have same problem as bmp. Error: "Object reference not set to an instance of an object." Can somebody help me!?

Thank you.

Dec 1, 2011 at 6:27 PM

Very useful code!

I modified this code slightly in order to allow ContextMenues as well as tags. I changed the base class and added the code suggested by davidsvaughn:

    public class EdgeContentPresenter : ContentPresenter
    {
        public EdgeContentPresenter()
        {
            LayoutUpdated += new EventHandler(EdgeContentPresenter_LayoutUpdated);
        }

        private EdgeControl GetEdgeControl(DependencyObject parent)
        {
            while (parent != null)
                if (parent is EdgeControl)
                    return (EdgeControl)parent;
                else
                    parent = VisualTreeHelper.GetParent(parent);
            return null;
        }

        private static double GetAngleBetweenPoints(Point point1, Point point2)
        {
            return Math.Atan2(point1.Y - point2.Y, point2.X - point1.X);
        }

        private static double GetDistanceBetweenPoints(Point point1, Point point2)
        {
            return Math.Sqrt(Math.Pow(point2.X - point1.X, 2) + Math.Pow(point2.Y - point1.Y, 2));
        }

        private static double GetLabelDistance(double edgeLength)
        {
            return edgeLength / 2;  // set the label halfway the length of the edge
        }

        void EdgeContentPresenter_LayoutUpdated(object sender, EventArgs e)
        {
            if (!IsLoaded)
                return;
            var edgeControl = GetEdgeControl(VisualParent);
            if (edgeControl == null)
                return;
            var source = edgeControl.Source;
            var p1 = new Point(GraphCanvas.GetX(source), GraphCanvas.GetY(source));
            var target = edgeControl.Target;
            var p2 = new Point(GraphCanvas.GetX(target), GraphCanvas.GetY(target));

            double edgeLength;
            var routePoints = edgeControl.RoutePoints;
            if (routePoints == null)
                // the edge is a single segment (p1,p2)
                edgeLength = GetLabelDistance(GetDistanceBetweenPoints(p1, p2));
            else
            {
                // the edge has one or more segments
                // compute the total length of all the segments
                edgeLength = 0;
                for (int i = 0; i <= routePoints.Length; ++i)
                    if (i == 0)
                        edgeLength += GetDistanceBetweenPoints(p1, routePoints[0]);
                    else if (i == routePoints.Length)
                        edgeLength += GetDistanceBetweenPoints(routePoints[routePoints.Length - 1], p2);
                    else
                        edgeLength += GetDistanceBetweenPoints(routePoints[i - 1], routePoints[i]);
                // find the line segment where the half distance is located
                edgeLength = GetLabelDistance(edgeLength);
                Point newp1 = p1;
                Point newp2 = p2;
                for (int i = 0; i <= routePoints.Length; ++i)
                {
                    double lengthOfSegment;
                    if (i == 0)
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = p1, newp2 = routePoints[0]);
                    else if (i == routePoints.Length)
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = routePoints[routePoints.Length - 1], newp2 = p2);
                    else
                        lengthOfSegment = GetDistanceBetweenPoints(newp1 = routePoints[i - 1], newp2 = routePoints[i]);
                    if (lengthOfSegment >= edgeLength)
                        break;
                    edgeLength -= lengthOfSegment;
                }
                // redefine our edge points
                p1 = newp1;
                p2 = newp2;
            }
            // align the point so that it  passes through the center of the label content
            var p = p1;
            var desiredSize = DesiredSize;
            p.Offset(-desiredSize.Width / 2, -desiredSize.Height / 2);

            // move it "edgLength" on the segment
            var angleBetweenPoints = GetAngleBetweenPoints(p1, p2);
            //p.Offset(edgeLength * Math.Cos(angleBetweenPoints), -edgeLength * Math.Sin(angleBetweenPoints));
            float x = 12.5f, y = 12.5f;
            double sin = Math.Sin(angleBetweenPoints);
            double cos = Math.Cos(angleBetweenPoints);
            double sign = sin * cos / Math.Abs(sin * cos);
            p.Offset(x * sin * sign + edgeLength * cos, y * cos * sign - edgeLength * sin);
            Arrange(new Rect(p, desiredSize));
        }

    }

This is the modified EdgeControl:

        <Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />
        <Style TargetType="{x:Type graphsharp:EdgeControl}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type graphsharp:EdgeControl}">
                        <Canvas>
                            <Path Stroke="{TemplateBinding Foreground}"
                                StrokeThickness="{TemplateBinding StrokeThickness}"
                                MinWidth="1"
                                MinHeight="1"
                                ToolTip="{TemplateBinding ToolTip}"
                                x:Name="edgePath">
                                <Path.Data>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <MultiBinding Converter="{StaticResource routeToPathConverter}">
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(graphsharp:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(graphsharp:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(graphsharp:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(graphsharp:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="RoutePoints" />
                                            </MultiBinding>
                                        </PathGeometry.Figures>
                                    </PathGeometry>
                                </Path.Data>
                            </Path>
                            <local:EdgeContentPresenter Content="{TemplateBinding Edge}" />
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="graphsharp:GraphElementBehaviour.HighlightTrigger"
                Value="{Binding RelativeSource={RelativeSource Self}, Path=IsMouseOver}" />
            <Setter Property="MinWidth"
                Value="3" />
            <Setter Property="MinHeight"
                Value="3" />
            <Setter Property="Background"
                Value="Red" />
            <Setter Property="Foreground"
                Value="Silver" />
            <Setter Property="Opacity"
                Value="0.5" />
            <Style.Triggers>
                <Trigger Property="graphsharp:GraphElementBehaviour.IsHighlighted"
                     Value="True">
                    <Setter Property="Foreground"
                        Value="Black" />
                </Trigger>
                <Trigger Property="graphsharp:GraphElementBehaviour.IsSemiHighlighted"
                     Value="True">
                    <Setter Property="Foreground"
                        Value="Yellow" />
                </Trigger>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="graphsharp:GraphElementBehaviour.IsSemiHighlighted"
                               Value="True" />
                        <Condition Property="graphsharp:GraphElementBehaviour.SemiHighlightInfo"
                               Value="InEdge" />
                    </MultiTrigger.Conditions>
                    <Setter Property="Foreground"
                        Value="Red" />
                </MultiTrigger>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="graphsharp:GraphElementBehaviour.IsSemiHighlighted"
                               Value="True" />
                        <Condition Property="graphsharp:GraphElementBehaviour.SemiHighlightInfo"
                               Value="OutEdge" />
                    </MultiTrigger.Conditions>
                    <Setter Property="Foreground"
                        Value="Blue" />
                </MultiTrigger>
            </Style.Triggers>
        </Style>

Rendering of my Edge class:

        <DataTemplate DataType="{x:Type local:ServerClientEdge}">
            <TextBlock Text="{Binding Path=Src.id}" >
                <TextBlock.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Delete" ToolTip="Choose to delete ..." ToolTipService.ShowOnDisabled="True" Command="{x:Static local:GraphViewModel.DeleteServerScriptToClientScriptCommand}" CommandParameter="{Binding Path=Src}"/>
                        <MenuItem Header="Foreach" ToolTip="Choose to enable ..." ToolTipService.ShowOnDisabled="True" Command="{x:Static local:GraphViewModel.ForeachServerScriptToClientScriptCommand}" CommandParameter="{Binding Path=Src}"/>
                        <MenuItem Header="Condition" ToolTip="Choose to edit ..." ToolTipService.ShowOnDisabled="True" Command="{x:Static local:GraphViewModel.ConditionServerScriptToClientScriptCommand}" CommandParameter="{Binding Path=Src}"/>
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </DataTemplate>

I have different types of edge classes, all based on :

public class CmdbEdge : Edge<object>
    {
        public CmdbEdge(object source, object target)
            : base(source, target)
        {
        }
    }

This is the one shown above:

    public class ServerClientEdge : CmdbEdge
    {
        public ServerScripts_ClientScripts Src { get; set; }

        public ServerClientEdge(ServerScripts_ClientScripts sscs)
            : base(sscs.ServerScript1, sscs.ClientScript1)
        {
            Src = sscs;
        }

    }

Dec 14, 2011 at 5:49 AM

somebody can upload the working library and samples with labels on the edges please

Apr 24, 2012 at 7:45 PM

I'm getting the following error for this line of code in the xaml file

<Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />

The error is, "A value of type EdgeRouteToPathConverter cannot be added to a collection or dictionary of type UIElementCollection"

Any ideas please?

Dec 11, 2012 at 5:41 PM

Very useful code.

I applied the code and it worked. But I have a problem, I need the labels to be aligned with the degree of the edge.

Can somebody help me please?

Thanks

Coordinator
Dec 11, 2012 at 10:37 PM
Edited Dec 11, 2012 at 10:37 PM

For this one that stuff would do the trick: http://www.codeproject.com/Articles/30090/Text-On-A-Path-in-WPF

Modify the ControlTemplate of the EdgeControl using the stuff in the codeproject article.

Jan 28, 2013 at 3:01 PM

Hi, I can't manage how to use your code. There's some example somewhere? I've created all the class and I get no errors, but I can't see nothing on screen!

May 9, 2013 at 9:49 AM
This code works fine:
<Window x:Class="Finite.OutputWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Finite.Graphing"
        xmlns:graphsharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls"
        xmlns:Converters="clr-namespace:GraphSharp.Converters;assembly=GraphSharp.Controls"
        xmlns:WPFExtensions_AttachedBehaviours="clr-namespace:WPFExtensions.AttachedBehaviours;assembly=WPFExtensions"
        Title="OutputWindow" Height="300" Width="300"
        x:Name="root">
    
    <Grid>
        <graphsharp:GraphLayout x:Name="graphLayout"
                                Graph="{Binding ElementName=root,Path=GraphToVizualize}"
                                LayoutAlgorithmType="Tree"
                                OverlapRemovalAlgorithmType="FSA"
                                />
    </Grid>
</Window>
However, when I add resources, I get an error
XamlParseException: Prefix 'graphsharp' does not map to a namespace.
<Window x:Class="Finite.OutputWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Finite.Graphing"
        xmlns:graphsharp="clr-namespace:GraphSharp.Controls;assembly=GraphSharp.Controls"
        xmlns:Converters="clr-namespace:GraphSharp.Converters;assembly=GraphSharp.Controls"
        xmlns:WPFExtensions_AttachedBehaviours="clr-namespace:WPFExtensions.AttachedBehaviours;assembly=WPFExtensions"
        Title="OutputWindow" Height="300" Width="300"
        x:Name="root">
    <Window.Resources>
        <Converters:EdgeRouteToPathConverter x:Key="routeToPathConverter" />

        <Style TargetType="{x:Type graphsharp:EdgeControl}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type graphsharp:EdgeControl}">
                        <Canvas>
                            <Path Stroke="{TemplateBinding Foreground}"
                          StrokeThickness="2"
                          MinWidth="1"
                          MinHeight="1"
                          x:Name="edgePath">
                                <Path.Data>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <MultiBinding Converter="{StaticResource routeToPathConverter}">
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(graphsharp:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.(graphsharp:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Source.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                Path="Source.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(graphsharp:GraphCanvas.X)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.(graphsharp:GraphCanvas.Y)" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualWidth" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="Target.ActualHeight" />
                                                <Binding RelativeSource="{RelativeSource TemplatedParent}"
                                                 Path="RoutePoints" />
                                            </MultiBinding>
                                        </PathGeometry.Figures>
                                    </PathGeometry>
                                </Path.Data>
                            </Path>
                            <local:EdgeLabelControl Content="{Binding Edge.Tag, RelativeSource={RelativeSource TemplatedParent}}" />
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    
    <Grid>
        <graphsharp:GraphLayout x:Name="graphLayout"
                                Graph="{Binding ElementName=root,Path=GraphToVizualize}"
                                LayoutAlgorithmType="Tree"
                                OverlapRemovalAlgorithmType="FSA"
                                />
    </Grid>
</Window>
What am I doing wrong?
May 15, 2013 at 6:31 AM
This is a known VS2012 error. When you add XAML reference to a namespace in an external dll library visual designer get screwed however code compiles and works fine. Use VS2010 if you need designer features in this case until VS2012 is fixed.