From 49469250b32f808c1440dc35a42c84c52ce65be0 Mon Sep 17 00:00:00 2001 From: chtenb Date: Wed, 9 Aug 2023 14:55:54 +0200 Subject: [PATCH] Use file scoped namespaces (#63) --- README.md | 393 +++--- .../CGraphBasicOperations.cs | 251 ++-- Rubjerg.Graphviz.Test/CGraphEdgeCases.cs | 649 +++++----- .../CGraphIntegrationTests.cs | 243 ++-- Rubjerg.Graphviz.Test/CGraphStressTests.cs | 13 +- Rubjerg.Graphviz.Test/OldTutorial.cs | 353 +++--- Rubjerg.Graphviz.Test/Reproductions.cs | 216 ++-- Rubjerg.Graphviz.Test/TestDotLayout.cs | 509 ++++---- Rubjerg.Graphviz.Test/TestInterop.cs | 53 +- Rubjerg.Graphviz.Test/TestXDotLayout.cs | 179 ++- Rubjerg.Graphviz.Test/Tutorial.cs | 393 +++--- Rubjerg.Graphviz.Test/Utils.cs | 127 +- .../Rubjerg.Graphviz.TransitiveTest.csproj | 1 + .../TestReference.cs | 25 +- Rubjerg.Graphviz/CGraphThing.cs | 371 +++--- Rubjerg.Graphviz/Edge.cs | 297 +++-- Rubjerg.Graphviz/ForeignFunctionInterface.cs | 1091 ++++++++-------- Rubjerg.Graphviz/Graph.cs | 1111 ++++++++--------- Rubjerg.Graphviz/GraphComparer.cs | 141 ++- Rubjerg.Graphviz/GraphVizLabel.cs | 103 +- Rubjerg.Graphviz/GraphVizThing.cs | 95 +- Rubjerg.Graphviz/GraphvizCommand.cs | 111 +- Rubjerg.Graphviz/LayoutEngines.cs | 23 +- Rubjerg.Graphviz/NativeMethods.cs | 49 +- Rubjerg.Graphviz/Node.cs | 469 ++++--- Rubjerg.Graphviz/RootGraph.cs | 151 ++- Rubjerg.Graphviz/SubGraph.cs | 103 +- Rubjerg.Graphviz/XDot.cs | 423 ++++--- Rubjerg.Graphviz/XDotFFI.cs | 286 +++-- Rubjerg.Graphviz/XDotParser.cs | 591 +++++---- 30 files changed, 4393 insertions(+), 4427 deletions(-) diff --git a/README.md b/README.md index 091d8ac..b40e9f1 100644 --- a/README.md +++ b/README.md @@ -53,220 +53,219 @@ using NUnit.Framework; using System.Drawing; using System.Linq; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class Tutorial { - [TestFixture()] - public class Tutorial + public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; + public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; + public const string SplinePattern = + @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; + + [Test, Order(1)] + public void GraphConstruction() { - public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; - public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; - public const string SplinePattern = - @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; + // You can programmatically construct graphs as follows + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); + // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, + // except it is recorded and preserved when the graph is written as a file. + + // The node names are unique identifiers within a graph in Graphviz + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + + // The edge name is only unique between two nodes + Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); + Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); + Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); + + // An edge name is optional and omitting it will result in a new nameless edge. + // There can be multiple nameless edges between any two nodes. + Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); + Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); + Assert.AreNotEqual(edgeAB1, edgeAB2); + + // We can attach attributes to nodes, edges and graphs to store information and instruct + // Graphviz by specifying layout parameters. At the moment we only support string + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, + // or edges) have the same attributes. An attribute has to be introduced with a default value + // first for a certain kind, before we can use it. + Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); + nodeA.SetAttribute("my attribute", "othervalue"); + + // Attributes are introduced per kind (Node, Edge, Graph) per root graph. + // So to be able to use "my attribute" on edges, we first have to introduce it as well. + Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); + edgeAB.SetAttribute("my attribute", "othervalue"); + + // To introduce and set an attribute at the same time, there are convenience wrappers + edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); + // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. + edgeBC.SetAttribute("new attr", "value"); + + // Some attributes - like "label" - accept HTML strings as value + // To tell Graphviz that a string should be interpreted as HTML use the designated methods + Node.IntroduceAttribute(root, "label", "defaultlabel"); + nodeB.SetAttributeHtml("label", "Some HTML string"); + + // We can simply export this graph to a text file in dot format + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // A word of advice, Graphviz doesn't play very well with empty strings. + // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + } - [Test, Order(1)] - public void GraphConstruction() + [Test, Order(2)] + public void Layouting() + { + // If we have a given dot file (in this case the one we generated above), we can also read it back in + RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // We can ask Graphviz to compute a layout and render it to svg + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); + + // We can use layout engines other than dot by explicitly passing the engine we want + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); + + // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes + // This will create a copy of our original graph with layout information attached to it in the form + // of attributes. + RootGraph layout = root.CreateLayout(); + + // There are convenience methods available that parse these attributes for us and give + // back the layout information in an accessible form. + Node nodeA = layout.GetNode("A"); + PointF position = nodeA.GetPosition(); + Utils.AssertPattern(PointPattern, position.ToString()); + + RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); + + // Or splines between nodes + Node nodeB = layout.GetNode("B"); + Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); + PointF[] spline = edge.GetFirstSpline(); + string splineString = string.Join(", ", spline.Select(p => p.ToString())); + Utils.AssertPattern(SplinePattern, splineString); + + // If we require detailed drawing information for any object, we can retrieve the so called "xdot" + // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. + var activeColor = Color.Black; + foreach (var op in nodeA.GetDrawing()) { - // You can programmatically construct graphs as follows - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); - // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, - // except it is recorded and preserved when the graph is written as a file. - - // The node names are unique identifiers within a graph in Graphviz - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - - // The edge name is only unique between two nodes - Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); - Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); - Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); - - // An edge name is optional and omitting it will result in a new nameless edge. - // There can be multiple nameless edges between any two nodes. - Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); - Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); - Assert.AreNotEqual(edgeAB1, edgeAB2); - - // We can attach attributes to nodes, edges and graphs to store information and instruct - // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, - // or edges) have the same attributes. An attribute has to be introduced with a default value - // first for a certain kind, before we can use it. - Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); - nodeA.SetAttribute("my attribute", "othervalue"); - - // Attributes are introduced per kind (Node, Edge, Graph) per root graph. - // So to be able to use "my attribute" on edges, we first have to introduce it as well. - Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); - edgeAB.SetAttribute("my attribute", "othervalue"); - - // To introduce and set an attribute at the same time, there are convenience wrappers - edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. - edgeBC.SetAttribute("new attr", "value"); - - // Some attributes - like "label" - accept HTML strings as value - // To tell Graphviz that a string should be interpreted as HTML use the designated methods - Node.IntroduceAttribute(root, "label", "defaultlabel"); - nodeB.SetAttributeHtml("label", "Some HTML string"); - - // We can simply export this graph to a text file in dot format - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // A word of advice, Graphviz doesn't play very well with empty strings. - // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + if (op is XDotOp.FillColor { Value: string htmlColor }) + { + activeColor = ColorTranslator.FromHtml(htmlColor); + } + else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) + { + var boundingBox = filledEllipse.ToRectangleF(); + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + } + // Handle any xdot operation you require } - [Test, Order(2)] - public void Layouting() + var activeFont = XDotFont.Default; + foreach (var op in nodeA.GetDrawing()) { - // If we have a given dot file (in this case the one we generated above), we can also read it back in - RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // We can ask Graphviz to compute a layout and render it to svg - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - - // We can use layout engines other than dot by explicitly passing the engine we want - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - - // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes - // This will create a copy of our original graph with layout information attached to it in the form - // of attributes. - RootGraph layout = root.CreateLayout(); - - // There are convenience methods available that parse these attributes for us and give - // back the layout information in an accessible form. - Node nodeA = layout.GetNode("A"); - PointF position = nodeA.GetPosition(); - Utils.AssertPattern(PointPattern, position.ToString()); - - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); - Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); - - // Or splines between nodes - Node nodeB = layout.GetNode("B"); - Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); - string splineString = string.Join(", ", spline.Select(p => p.ToString())); - Utils.AssertPattern(SplinePattern, splineString); - - // If we require detailed drawing information for any object, we can retrieve the so called "xdot" - // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = Color.Black; - foreach (var op in nodeA.GetDrawing()) + if (op is XDotOp.Font { Value: var font }) { - if (op is XDotOp.FillColor { Value: string htmlColor }) - { - activeColor = ColorTranslator.FromHtml(htmlColor); - } - else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) - { - var boundingBox = filledEllipse.ToRectangleF(); - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - } - // Handle any xdot operation you require + activeFont = font; + Utils.AssertPattern(@"Times-Roman", font.Name); } - - var activeFont = XDotFont.Default; - foreach (var op in nodeA.GetDrawing()) + else if (op is XDotOp.Text { Value: var text }) { - if (op is XDotOp.Font { Value: var font }) - { - activeFont = font; - Utils.AssertPattern(@"Times-Roman", font.Name); - } - else if (op is XDotOp.Text { Value: var text }) - { - var anchor = text.Anchor(); - Utils.AssertPattern(PointPattern, anchor.ToString()); - var boundingBox = text.TextBoundingBox(activeFont); - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - Assert.AreEqual(text.Text, "A"); - } - // Handle any xdot operation you require + var anchor = text.Anchor(); + Utils.AssertPattern(PointPattern, anchor.ToString()); + var boundingBox = text.TextBoundingBox(activeFont); + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + Assert.AreEqual(text.Text, "A"); } - - // These are just simple examples to showcase the structure of xdot operations. - // In reality the information can be much richer and more complex. + // Handle any xdot operation you require } - [Test, Order(3)] - public void Clusters() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // When a subgraph name is prefixed with cluster, - // the dot layout engine will render it as a box around the containing nodes. - SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); - cluster1.AddExisting(nodeB); - cluster1.AddExisting(nodeC); - SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); - cluster2.AddExisting(nodeD); - - // COMPOUND EDGES - // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this - // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it - // at the border of the cluster. We provide convenience methods for this. - // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". - Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail - // The boolean indicates whether the dummy node should take up any space. When you pass - // false and you have a lot of edges, the edges may start to overlap a lot. - _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); - _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); - _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); - - var layout = root.CreateLayout(); - - SubGraph cluster = layout.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = layout.GetBoundingBox(); - Utils.AssertPattern(RectPattern, clusterbox.ToString()); - Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); - } + // These are just simple examples to showcase the structure of xdot operations. + // In reality the information can be much richer and more complex. + } - [Test, Order(4)] - public void Records() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); - Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + [Test, Order(3)] + public void Clusters() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // When a subgraph name is prefixed with cluster, + // the dot layout engine will render it as a box around the containing nodes. + SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); + cluster1.AddExisting(nodeB); + cluster1.AddExisting(nodeC); + SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); + cluster2.AddExisting(nodeD); + + // COMPOUND EDGES + // Graphviz does not really support edges from and to clusters. However, by adding an + // invisible dummynode and setting the ltail or lhead attributes of an edge this + // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it + // at the border of the cluster. We provide convenience methods for this. + // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". + Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail + // The boolean indicates whether the dummy node should take up any space. When you pass + // false and you have a lot of edges, the edges may start to overlap a lot. + _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); + _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); + _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); + + var layout = root.CreateLayout(); + + SubGraph cluster = layout.GetSubgraph("cluster_1"); + RectangleF clusterbox = cluster.GetBoundingBox(); + RectangleF rootgraphbox = layout.GetBoundingBox(); + Utils.AssertPattern(RectPattern, clusterbox.ToString()); + Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); + } - var layout = root.CreateLayout(); + [Test, Order(4)] + public void Records() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); + Node nodeA = root.GetOrAddNode("A"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - // The order of the list matches the order in which the labels occur in the label string above. - var rects = layout.GetNode("A").GetRecordRectangles().ToList(); - Assert.AreEqual(9, rects.Count); - } + var layout = root.CreateLayout(); - [Test, Order(5)] - public void StringEscaping() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); - Node.IntroduceAttribute(root, "label", "\\N"); - Node nodeA = root.GetOrAddNode("A"); - - // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for you to do just that. - nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - - // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. - // This can be troubling if you have an externally defined ID for such a port. - // We provide a function that maps strings to valid portnames. - var somePortId = "port id with :| special characters"; - var validPortName = Edge.ConvertUidToPortName(somePortId); - Node nodeB = root.GetOrAddNode("B"); - nodeB.SafeSetAttribute("shape", "record", ""); - nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); - - // The conversion function makes sure different strings don't accidentally map onto the same portname - Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); - } + // The order of the list matches the order in which the labels occur in the label string above. + var rects = layout.GetNode("A").GetRecordRectangles().ToList(); + Assert.AreEqual(9, rects.Count); + } + + [Test, Order(5)] + public void StringEscaping() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); + Node.IntroduceAttribute(root, "label", "\\N"); + Node nodeA = root.GetOrAddNode("A"); + + // Several characters and character sequences can have special meanings in labels, like \N. + // When you want to have a literal string in a label, we provide a convenience function for you to do just that. + nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); + + // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. + // This can be troubling if you have an externally defined ID for such a port. + // We provide a function that maps strings to valid portnames. + var somePortId = "port id with :| special characters"; + var validPortName = Edge.ConvertUidToPortName(somePortId); + Node nodeB = root.GetOrAddNode("B"); + nodeB.SafeSetAttribute("shape", "record", ""); + nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); + + // The conversion function makes sure different strings don't accidentally map onto the same portname + Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); } } ``` diff --git a/Rubjerg.Graphviz.Test/CGraphBasicOperations.cs b/Rubjerg.Graphviz.Test/CGraphBasicOperations.cs index c29c85a..53bad57 100644 --- a/Rubjerg.Graphviz.Test/CGraphBasicOperations.cs +++ b/Rubjerg.Graphviz.Test/CGraphBasicOperations.cs @@ -2,155 +2,154 @@ using System.Linq; using NUnit.Framework; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class CGraphBasicOperations { - [TestFixture()] - public class CGraphBasicOperations + [Test()] + public void TestCopyAttributes() { - [Test()] - public void TestCopyAttributes() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node n1 = root.GetOrAddNode("1"); - Node.IntroduceAttribute(root, "test", "foo"); - Assert.AreEqual("foo", n1.GetAttribute("test")); - n1.SetAttribute("test", "bar"); - Assert.AreEqual("bar", n1.GetAttribute("test")); - - RootGraph root2 = Utils.CreateUniqueTestGraph(); - Node n2 = root2.GetOrAddNode("2"); - Assert.AreEqual(null, n2.GetAttribute("test")); - Assert.AreEqual(0, n1.CopyAttributesTo(n2)); - Assert.AreEqual("bar", n2.GetAttribute("test")); - } + RootGraph root = Utils.CreateUniqueTestGraph(); + Node n1 = root.GetOrAddNode("1"); + Node.IntroduceAttribute(root, "test", "foo"); + Assert.AreEqual("foo", n1.GetAttribute("test")); + n1.SetAttribute("test", "bar"); + Assert.AreEqual("bar", n1.GetAttribute("test")); + + RootGraph root2 = Utils.CreateUniqueTestGraph(); + Node n2 = root2.GetOrAddNode("2"); + Assert.AreEqual(null, n2.GetAttribute("test")); + Assert.AreEqual(0, n1.CopyAttributesTo(n2)); + Assert.AreEqual("bar", n2.GetAttribute("test")); + } - [Test()] - public void TestDeletions() - { - RootGraph root = Utils.CreateUniqueTestGraph(); + [Test()] + public void TestDeletions() + { + RootGraph root = Utils.CreateUniqueTestGraph(); - Node tail = root.GetOrAddNode("1"); - Node head = root.GetOrAddNode("2"); - Node other = root.GetOrAddNode("3"); + Node tail = root.GetOrAddNode("1"); + Node head = root.GetOrAddNode("2"); + Node other = root.GetOrAddNode("3"); - Edge edge = root.GetOrAddEdge(tail, head, "edge"); - Edge tailout = root.GetOrAddEdge(tail, other, "tailout"); - Edge headout = root.GetOrAddEdge(head, other, "headout"); - Edge tailin = root.GetOrAddEdge(other, tail, "tailin"); - Edge headin = root.GetOrAddEdge(other, head, "headin"); + Edge edge = root.GetOrAddEdge(tail, head, "edge"); + Edge tailout = root.GetOrAddEdge(tail, other, "tailout"); + Edge headout = root.GetOrAddEdge(head, other, "headout"); + Edge tailin = root.GetOrAddEdge(other, tail, "tailin"); + Edge headin = root.GetOrAddEdge(other, head, "headin"); - Assert.IsTrue(root.Equals(root.MyRootGraph)); - Assert.IsTrue(root.Equals(tail.MyRootGraph)); - Assert.IsTrue(root.Equals(edge.MyRootGraph)); + Assert.IsTrue(root.Equals(root.MyRootGraph)); + Assert.IsTrue(root.Equals(tail.MyRootGraph)); + Assert.IsTrue(root.Equals(edge.MyRootGraph)); - Assert.AreEqual(3, tail.TotalDegree()); - Assert.AreEqual(3, head.TotalDegree()); - Assert.AreEqual(3, root.Nodes().Count()); + Assert.AreEqual(3, tail.TotalDegree()); + Assert.AreEqual(3, head.TotalDegree()); + Assert.AreEqual(3, root.Nodes().Count()); - root.Delete(edge); + root.Delete(edge); - Assert.AreEqual(2, tail.TotalDegree()); - Assert.AreEqual(2, head.TotalDegree()); - Assert.AreEqual(3, root.Nodes().Count()); + Assert.AreEqual(2, tail.TotalDegree()); + Assert.AreEqual(2, head.TotalDegree()); + Assert.AreEqual(3, root.Nodes().Count()); - root.Delete(tail); + root.Delete(tail); - Assert.AreEqual(2, root.Nodes().Count()); - Assert.AreEqual(2, other.TotalDegree()); - } + Assert.AreEqual(2, root.Nodes().Count()); + Assert.AreEqual(2, other.TotalDegree()); + } - [Test()] - public void TestNodeMerge() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node merge = root.GetOrAddNode("merge"); - Node target = root.GetOrAddNode("target"); - Node other = root.GetOrAddNode("other"); - - Edge selfloop = root.GetOrAddEdge(merge, merge, "selfloop"); - Edge contracted = root.GetOrAddEdge(merge, target, "contracted"); - Edge counter = root.GetOrAddEdge(target, merge, "counter"); - Edge mergeout = root.GetOrAddEdge(merge, other, "mergeout"); - Edge targetout = root.GetOrAddEdge(target, other, "targetout"); - Edge mergein = root.GetOrAddEdge(other, merge, "mergein"); - Edge targetin = root.GetOrAddEdge(other, target, "targetin"); - - Assert.AreEqual(6, merge.TotalDegree()); - Assert.AreEqual(4, target.TotalDegree()); - Assert.AreEqual(3, root.Nodes().Count()); - - //root.ComputeDotLayout(); - //root.ToSvgFile("dump1.svg"); - //root.FreeLayout(); - //root.ToDotFile("dump1.dot"); - - root.Merge(merge, target); - - //root.ComputeDotLayout(); - //root.ToSvgFile("dump2.svg"); - //root.FreeLayout(); - //root.ToDotFile("dump2.dot"); - - Assert.AreEqual(2, root.Nodes().Count()); - Assert.AreEqual(3, target.InDegree()); - Assert.AreEqual(3, target.OutDegree()); - Assert.AreEqual(2, other.InDegree()); - Assert.AreEqual(2, other.OutDegree()); - } + [Test()] + public void TestNodeMerge() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node merge = root.GetOrAddNode("merge"); + Node target = root.GetOrAddNode("target"); + Node other = root.GetOrAddNode("other"); + + Edge selfloop = root.GetOrAddEdge(merge, merge, "selfloop"); + Edge contracted = root.GetOrAddEdge(merge, target, "contracted"); + Edge counter = root.GetOrAddEdge(target, merge, "counter"); + Edge mergeout = root.GetOrAddEdge(merge, other, "mergeout"); + Edge targetout = root.GetOrAddEdge(target, other, "targetout"); + Edge mergein = root.GetOrAddEdge(other, merge, "mergein"); + Edge targetin = root.GetOrAddEdge(other, target, "targetin"); + + Assert.AreEqual(6, merge.TotalDegree()); + Assert.AreEqual(4, target.TotalDegree()); + Assert.AreEqual(3, root.Nodes().Count()); + + //root.ComputeDotLayout(); + //root.ToSvgFile("dump1.svg"); + //root.FreeLayout(); + //root.ToDotFile("dump1.dot"); + + root.Merge(merge, target); + + //root.ComputeDotLayout(); + //root.ToSvgFile("dump2.svg"); + //root.FreeLayout(); + //root.ToDotFile("dump2.dot"); + + Assert.AreEqual(2, root.Nodes().Count()); + Assert.AreEqual(3, target.InDegree()); + Assert.AreEqual(3, target.OutDegree()); + Assert.AreEqual(2, other.InDegree()); + Assert.AreEqual(2, other.OutDegree()); + } - [Test()] - public void TestEdgeContraction() + [Test()] + public void TestEdgeContraction() + { + //NativeMethods.AllocConsole(); + RootGraph root = Utils.CreateUniqueTestGraph(); + Node tail = root.GetOrAddNode("x"); + Node head = root.GetOrAddNode("xx"); + Node other = root.GetOrAddNode("xxx"); + + Edge contracted = root.GetOrAddEdge(tail, head, "tocontract"); + Edge parallel = root.GetOrAddEdge(tail, head, "parallel"); + Edge counterparallel = root.GetOrAddEdge(head, tail, "counterparallel"); + Edge tailout = root.GetOrAddEdge(tail, other, "tailout"); + Edge headout = root.GetOrAddEdge(head, other, "headout"); + Edge tailin = root.GetOrAddEdge(other, tail, "tailin"); + Edge headin = root.GetOrAddEdge(other, head, "headin"); + + foreach (Node n in root.Nodes()) { - //NativeMethods.AllocConsole(); - RootGraph root = Utils.CreateUniqueTestGraph(); - Node tail = root.GetOrAddNode("x"); - Node head = root.GetOrAddNode("xx"); - Node other = root.GetOrAddNode("xxx"); - - Edge contracted = root.GetOrAddEdge(tail, head, "tocontract"); - Edge parallel = root.GetOrAddEdge(tail, head, "parallel"); - Edge counterparallel = root.GetOrAddEdge(head, tail, "counterparallel"); - Edge tailout = root.GetOrAddEdge(tail, other, "tailout"); - Edge headout = root.GetOrAddEdge(head, other, "headout"); - Edge tailin = root.GetOrAddEdge(other, tail, "tailin"); - Edge headin = root.GetOrAddEdge(other, head, "headin"); - - foreach (Node n in root.Nodes()) + n.SafeSetAttribute("label", n.GetName(), "no"); + n.SafeSetAttribute("fontname", "Arial", "Arial"); + foreach (Edge e in n.EdgesOut()) { - n.SafeSetAttribute("label", n.GetName(), "no"); - n.SafeSetAttribute("fontname", "Arial", "Arial"); - foreach (Edge e in n.EdgesOut()) - { - e.SafeSetAttribute("label", e.GetName(), "no"); - e.SafeSetAttribute("fontname", "Arial", "Arial"); - } + e.SafeSetAttribute("label", e.GetName(), "no"); + e.SafeSetAttribute("fontname", "Arial", "Arial"); } + } - Assert.AreEqual(5, tail.TotalDegree()); - Assert.AreEqual(5, head.TotalDegree()); - Assert.AreEqual(3, root.Nodes().Count()); + Assert.AreEqual(5, tail.TotalDegree()); + Assert.AreEqual(5, head.TotalDegree()); + Assert.AreEqual(3, root.Nodes().Count()); - Node contraction = root.Contract(contracted, "contraction result"); + Node contraction = root.Contract(contracted, "contraction result"); - foreach (Node n in root.Nodes()) + foreach (Node n in root.Nodes()) + { + n.SafeSetAttribute("label", n.GetName(), "no"); + n.SafeSetAttribute("fontname", "Arial", "Arial"); + foreach (Edge e in n.EdgesOut()) { - n.SafeSetAttribute("label", n.GetName(), "no"); - n.SafeSetAttribute("fontname", "Arial", "Arial"); - foreach (Edge e in n.EdgesOut()) - { - e.SafeSetAttribute("label", e.GetName(), "no"); - e.SafeSetAttribute("fontname", "Arial", "Arial"); - } + e.SafeSetAttribute("label", e.GetName(), "no"); + e.SafeSetAttribute("fontname", "Arial", "Arial"); } - - //Console.Read(); - Assert.AreEqual(2, root.Nodes().Count()); - Assert.AreEqual(2, contraction.InDegree()); - Assert.AreEqual(2, contraction.OutDegree()); - Assert.AreEqual(2, other.InDegree()); - Assert.AreEqual(2, other.OutDegree()); } + //Console.Read(); + Assert.AreEqual(2, root.Nodes().Count()); + Assert.AreEqual(2, contraction.InDegree()); + Assert.AreEqual(2, contraction.OutDegree()); + Assert.AreEqual(2, other.InDegree()); + Assert.AreEqual(2, other.OutDegree()); } + } diff --git a/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs b/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs index 3305001..0aee358 100644 --- a/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs +++ b/Rubjerg.Graphviz.Test/CGraphEdgeCases.cs @@ -1,23 +1,23 @@ using System.Linq; using NUnit.Framework; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class CGraphEdgeCases { - [TestFixture()] - public class CGraphEdgeCases + [Test()] + public void TestUniqueGraphs() { - [Test()] - public void TestUniqueGraphs() - { - var g1 = RootGraph.CreateNew(GraphType.Directed, "test"); - var g2 = RootGraph.CreateNew(GraphType.Directed, "test"); - Assert.AreNotEqual(g1, g2); - } + var g1 = RootGraph.CreateNew(GraphType.Directed, "test"); + var g2 = RootGraph.CreateNew(GraphType.Directed, "test"); + Assert.AreNotEqual(g1, g2); + } - [Test()] - public void TestReadDotFile() - { - RootGraph root = RootGraph.FromDotString(@" + [Test()] + public void TestReadDotFile() + { + RootGraph root = RootGraph.FromDotString(@" digraph test { """"; A; @@ -28,41 +28,41 @@ digraph test { A -> B; } "); - var nodes = root.Nodes().ToList(); - var edges = root.Edges().ToList(); - var names = edges.Select(e => e.GetName()); - // The attribute 'key' maps to the edgename - // Note that omitted key values will result in unique edges - Assert.AreEqual(1, names.Count(n => n == "edgename")); - Assert.AreEqual(2, names.Count(n => n == null)); - Assert.AreEqual(3, nodes.Count); - } + var nodes = root.Nodes().ToList(); + var edges = root.Edges().ToList(); + var names = edges.Select(e => e.GetName()); + // The attribute 'key' maps to the edgename + // Note that omitted key values will result in unique edges + Assert.AreEqual(1, names.Count(n => n == "edgename")); + Assert.AreEqual(2, names.Count(n => n == null)); + Assert.AreEqual(3, nodes.Count); + } - [Test()] - public void TestWriteDotFile() - { - var root = RootGraph.CreateNew(GraphType.Directed); - - // Vice versa, empty edge names and node names will result in unique objects as well - var E = root.GetOrAddNode(""); - var E2 = root.GetOrAddNode(""); - var N = root.GetOrAddNode(null); - var N2 = root.GetOrAddNode(null); - var A = root.GetOrAddNode("A"); - var B = root.GetOrAddNode("B"); - _ = root.GetOrAddEdge(A, B, null); - _ = root.GetOrAddEdge(A, B, null); - _ = root.GetOrAddEdge(A, B, ""); - _ = root.GetOrAddEdge(A, B, ""); - _ = root.GetOrAddEdge(A, B, "edge2"); - _ = root.GetOrAddEdge(A, B, "edge2"); - var dot = root.ToDotString(); - - // Passing null to GetEdge will return any edge between the given endpoints. - var edge = root.GetEdge(A, B, null); - Assert.Contains(edge.GetName(), new[] { null, "edge2" }); - - Utils.AssertPattern(@"digraph { + [Test()] + public void TestWriteDotFile() + { + var root = RootGraph.CreateNew(GraphType.Directed); + + // Vice versa, empty edge names and node names will result in unique objects as well + var E = root.GetOrAddNode(""); + var E2 = root.GetOrAddNode(""); + var N = root.GetOrAddNode(null); + var N2 = root.GetOrAddNode(null); + var A = root.GetOrAddNode("A"); + var B = root.GetOrAddNode("B"); + _ = root.GetOrAddEdge(A, B, null); + _ = root.GetOrAddEdge(A, B, null); + _ = root.GetOrAddEdge(A, B, ""); + _ = root.GetOrAddEdge(A, B, ""); + _ = root.GetOrAddEdge(A, B, "edge2"); + _ = root.GetOrAddEdge(A, B, "edge2"); + var dot = root.ToDotString(); + + // Passing null to GetEdge will return any edge between the given endpoints. + var edge = root.GetEdge(A, B, null); + Assert.Contains(edge.GetName(), new[] { null, "edge2" }); + + Utils.AssertPattern(@"digraph { node \[label=""\\N""\]; ""%\d+""; ""%\d+""; @@ -75,311 +75,310 @@ public void TestWriteDotFile() A -> B \[key=edge2\]; } ".Replace("\r", ""), dot); - } + } - [Test()] - public void TestAttributeReintroduction() - { - // Reintroducing graph attributes resets all values. - RootGraph root = Utils.CreateUniqueTestGraph(); - Graph.IntroduceAttribute(root, "test", "default"); - root.SetAttribute("test", "1"); - Assert.AreEqual("1", root.GetAttribute("test")); - Graph.IntroduceAttribute(root, "test", "default"); - // Now the value has been reset! - Assert.AreEqual("default", root.GetAttribute("test")); - - // This is not the case for nodes - Node node = root.GetOrAddNode("nodename"); - Node.IntroduceAttribute(root, "test", "default"); - node.SetAttribute("test", "1"); - Assert.AreEqual("1", node.GetAttribute("test")); - Node.IntroduceAttribute(root, "test", "default"); - Assert.AreEqual("1", node.GetAttribute("test")); - } + [Test()] + public void TestAttributeReintroduction() + { + // Reintroducing graph attributes resets all values. + RootGraph root = Utils.CreateUniqueTestGraph(); + Graph.IntroduceAttribute(root, "test", "default"); + root.SetAttribute("test", "1"); + Assert.AreEqual("1", root.GetAttribute("test")); + Graph.IntroduceAttribute(root, "test", "default"); + // Now the value has been reset! + Assert.AreEqual("default", root.GetAttribute("test")); + + // This is not the case for nodes + Node node = root.GetOrAddNode("nodename"); + Node.IntroduceAttribute(root, "test", "default"); + node.SetAttribute("test", "1"); + Assert.AreEqual("1", node.GetAttribute("test")); + Node.IntroduceAttribute(root, "test", "default"); + Assert.AreEqual("1", node.GetAttribute("test")); + } - [Test()] - public void TestAttributeDefaults() + [Test()] + public void TestAttributeDefaults() + { { - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node.IntroduceAttribute(root, "label", ""); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - nodeA.SetAttribute("label", "1"); - Assert.AreEqual("1", nodeA.GetAttribute("label")); - Assert.AreEqual("", nodeB.GetAttribute("label")); - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.gv"); - } - - // The empty label default is not exported, and the default default is \N. - // Related issue: https://gitlab.com/graphviz/graphviz/-/issues/1887 - { - var root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.gv"); - Node nodeA = root.GetNode("A"); - Node nodeB = root.GetNode("B"); - Assert.AreEqual("1", nodeA.GetAttribute("label")); - Assert.AreEqual("\\N", nodeB.GetAttribute("label")); - - root.ComputeLayout(); - Assert.AreEqual("1", nodeA.GetAttribute("label")); - Assert.AreEqual("\\N", nodeB.GetAttribute("label")); - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/out.svg"); - } + RootGraph root = Utils.CreateUniqueTestGraph(); + Node.IntroduceAttribute(root, "label", ""); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + nodeA.SetAttribute("label", "1"); + Assert.AreEqual("1", nodeA.GetAttribute("label")); + Assert.AreEqual("", nodeB.GetAttribute("label")); + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.gv"); } - [Test()] - public void TestGetUnintroducedAttributes() + // The empty label default is not exported, and the default default is \N. + // Related issue: https://gitlab.com/graphviz/graphviz/-/issues/1887 { - RootGraph root = Utils.CreateUniqueTestGraph(); - Assert.AreEqual(null, root.GetAttribute("test")); - Graph.IntroduceAttribute(root, "test", "foo"); - Assert.AreEqual("foo", root.GetAttribute("test")); + var root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.gv"); + Node nodeA = root.GetNode("A"); + Node nodeB = root.GetNode("B"); + Assert.AreEqual("1", nodeA.GetAttribute("label")); + Assert.AreEqual("\\N", nodeB.GetAttribute("label")); - // If we call set attribute first, the attribute is automatically introduced - root.SetAttribute("test2", "foo"); - Assert.AreEqual("foo", root.GetAttribute("test2")); + root.ComputeLayout(); + Assert.AreEqual("1", nodeA.GetAttribute("label")); + Assert.AreEqual("\\N", nodeB.GetAttribute("label")); + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/out.svg"); } + } - [Test()] - public void TestCopyUnintroducedAttributes() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node n1 = root.GetOrAddNode("1"); - Node.IntroduceAttribute(root, "test", "foo"); - Assert.AreEqual("foo", n1.GetAttribute("test")); - n1.SetAttribute("test", "bar"); - Assert.AreEqual("bar", n1.GetAttribute("test")); - Node.IntroduceAttribute(root, "test2", "foo2"); - Assert.AreEqual("foo2", n1.GetAttribute("test2")); - n1.SetAttribute("test2", "bar2"); - Assert.AreEqual("bar2", n1.GetAttribute("test2")); - - RootGraph root2 = Utils.CreateUniqueTestGraph(); - Node n2 = root2.GetOrAddNode("2"); - // Only introduce the second attr, and see whether it gets copied - Node.IntroduceAttribute(root, "test2", "foo2"); - Assert.AreEqual(null, n2.GetAttribute("test")); - // While one would expect test2 to be copied correctly, it isn't, as copying test failed before that. - //Assert.AreEqual("bar2", n2.GetAttribute("test2")); - Assert.AreEqual(null, n2.GetAttribute("test2")); - } + [Test()] + public void TestGetUnintroducedAttributes() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Assert.AreEqual(null, root.GetAttribute("test")); + Graph.IntroduceAttribute(root, "test", "foo"); + Assert.AreEqual("foo", root.GetAttribute("test")); + + // If we call set attribute first, the attribute is automatically introduced + root.SetAttribute("test2", "foo"); + Assert.AreEqual("foo", root.GetAttribute("test2")); + } + [Test()] + public void TestCopyUnintroducedAttributes() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node n1 = root.GetOrAddNode("1"); + Node.IntroduceAttribute(root, "test", "foo"); + Assert.AreEqual("foo", n1.GetAttribute("test")); + n1.SetAttribute("test", "bar"); + Assert.AreEqual("bar", n1.GetAttribute("test")); + Node.IntroduceAttribute(root, "test2", "foo2"); + Assert.AreEqual("foo2", n1.GetAttribute("test2")); + n1.SetAttribute("test2", "bar2"); + Assert.AreEqual("bar2", n1.GetAttribute("test2")); + + RootGraph root2 = Utils.CreateUniqueTestGraph(); + Node n2 = root2.GetOrAddNode("2"); + // Only introduce the second attr, and see whether it gets copied + Node.IntroduceAttribute(root, "test2", "foo2"); + Assert.AreEqual(null, n2.GetAttribute("test")); + // While one would expect test2 to be copied correctly, it isn't, as copying test failed before that. + //Assert.AreEqual("bar2", n2.GetAttribute("test2")); + Assert.AreEqual(null, n2.GetAttribute("test2")); + } - [Test()] - public void TestCopyToNewRoot() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node n1 = root.GetOrAddNode("1"); - Node.IntroduceAttribute(root, "test", "foo"); - Assert.AreEqual("foo", n1.GetAttribute("test")); - n1.SetAttribute("test", "bar"); - Assert.AreEqual("bar", n1.GetAttribute("test")); - - RootGraph root2 = Utils.CreateUniqueTestGraph(); - Node.IntroduceAttribute(root2, "test", "foo"); - Node n2 = n1.CopyToOtherRoot(root2); - Assert.AreEqual("1", n2.GetName()); - Assert.AreEqual("bar", n2.GetAttribute("test")); - } - [Test()] - public void TestNodeAndGraphWithSameName() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - SubGraph sub = root.GetOrAddSubgraph("name"); - Node node = sub.GetOrAddNode("name"); - Assert.True(root.Contains(sub)); - Assert.True(sub.Contains(node)); - } + [Test()] + public void TestCopyToNewRoot() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node n1 = root.GetOrAddNode("1"); + Node.IntroduceAttribute(root, "test", "foo"); + Assert.AreEqual("foo", n1.GetAttribute("test")); + n1.SetAttribute("test", "bar"); + Assert.AreEqual("bar", n1.GetAttribute("test")); + + RootGraph root2 = Utils.CreateUniqueTestGraph(); + Node.IntroduceAttribute(root2, "test", "foo"); + Node n2 = n1.CopyToOtherRoot(root2); + Assert.AreEqual("1", n2.GetName()); + Assert.AreEqual("bar", n2.GetAttribute("test")); + } - [Test()] - public void TestRootOfRoot() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - RootGraph root2 = root.MyRootGraph; - } + [Test()] + public void TestNodeAndGraphWithSameName() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + SubGraph sub = root.GetOrAddSubgraph("name"); + Node node = sub.GetOrAddNode("name"); + Assert.True(root.Contains(sub)); + Assert.True(sub.Contains(node)); + } - [Test()] - public void TestSelfLoopEnumeration() - { - RootGraph graph = Utils.CreateUniqueTestGraph(); - Node node = graph.GetOrAddNode("node 1"); - Node node2 = graph.GetOrAddNode("node 2"); - Edge edgein = graph.GetOrAddEdge(node2, node, "in"); - Edge edgeout = graph.GetOrAddEdge(node, node2, "out"); - Edge edgeself = graph.GetOrAddEdge(node, node, "self"); - - Assert.AreEqual(2, node.InDegree()); - Assert.AreEqual(2, node.OutDegree()); - Assert.AreEqual(4, node.TotalDegree()); - - Assert.AreEqual(2, node.EdgesIn().Count()); - Assert.AreEqual(2, node.EdgesOut().Count()); - Assert.AreEqual(3, node.Edges().Count()); - } + [Test()] + public void TestRootOfRoot() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + RootGraph root2 = root.MyRootGraph; + } - [Test()] - public void TestNonStrictGraph() - { - RootGraph graph = Utils.CreateUniqueTestGraph(); - Assert.False(graph.IsStrict()); - Assert.False(graph.IsUndirected()); - Assert.True(graph.IsDirected()); + [Test()] + public void TestSelfLoopEnumeration() + { + RootGraph graph = Utils.CreateUniqueTestGraph(); + Node node = graph.GetOrAddNode("node 1"); + Node node2 = graph.GetOrAddNode("node 2"); + Edge edgein = graph.GetOrAddEdge(node2, node, "in"); + Edge edgeout = graph.GetOrAddEdge(node, node2, "out"); + Edge edgeself = graph.GetOrAddEdge(node, node, "self"); + + Assert.AreEqual(2, node.InDegree()); + Assert.AreEqual(2, node.OutDegree()); + Assert.AreEqual(4, node.TotalDegree()); + + Assert.AreEqual(2, node.EdgesIn().Count()); + Assert.AreEqual(2, node.EdgesOut().Count()); + Assert.AreEqual(3, node.Edges().Count()); + } - Node node = graph.GetOrAddNode("node 1"); - Node node2 = graph.GetOrAddNode("node 2"); + [Test()] + public void TestNonStrictGraph() + { + RootGraph graph = Utils.CreateUniqueTestGraph(); + Assert.False(graph.IsStrict()); + Assert.False(graph.IsUndirected()); + Assert.True(graph.IsDirected()); - Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); - Edge edge2 = graph.GetOrAddEdge(node, node, "edge 2"); - Assert.AreNotEqual(edge.GetName(), edge2.GetName()); + Node node = graph.GetOrAddNode("node 1"); + Node node2 = graph.GetOrAddNode("node 2"); - Edge edge3 = graph.GetOrAddEdge(node, node2, "edge 3"); - Edge edge4 = graph.GetOrAddEdge(node, node2, "edge 4"); - Assert.AreNotEqual(edge3.GetName(), edge4.GetName()); - } + Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); + Edge edge2 = graph.GetOrAddEdge(node, node, "edge 2"); + Assert.AreNotEqual(edge.GetName(), edge2.GetName()); - [Test()] - public void TestEdgeEquals() - { - RootGraph graph = Utils.CreateUniqueTestGraph(); - Node node = graph.GetOrAddNode("node 1"); - Node node2 = graph.GetOrAddNode("node 2"); - Node node3 = graph.GetOrAddNode("node 3"); - Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); - Edge edge2 = graph.GetOrAddEdge(node, node, "edge 2"); - Edge edge3 = graph.GetOrAddEdge(node, node2, "edge 3"); - Edge edge4 = graph.GetOrAddEdge(node2, node3, "edge 4"); - - Assert.AreEqual(edge, edge); - Assert.AreNotEqual(edge, edge2); - Assert.AreNotEqual(edge, edge3); - Assert.AreNotEqual(edge, edge4); - Assert.AreEqual(edge.GetHashCode(), edge.GetHashCode()); - Assert.AreNotEqual(edge.GetHashCode(), edge2.GetHashCode()); - Assert.AreNotEqual(edge.GetHashCode(), edge3.GetHashCode()); - Assert.AreNotEqual(edge.GetHashCode(), edge4.GetHashCode()); - } + Edge edge3 = graph.GetOrAddEdge(node, node2, "edge 3"); + Edge edge4 = graph.GetOrAddEdge(node, node2, "edge 4"); + Assert.AreNotEqual(edge3.GetName(), edge4.GetName()); + } + + [Test()] + public void TestEdgeEquals() + { + RootGraph graph = Utils.CreateUniqueTestGraph(); + Node node = graph.GetOrAddNode("node 1"); + Node node2 = graph.GetOrAddNode("node 2"); + Node node3 = graph.GetOrAddNode("node 3"); + Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); + Edge edge2 = graph.GetOrAddEdge(node, node, "edge 2"); + Edge edge3 = graph.GetOrAddEdge(node, node2, "edge 3"); + Edge edge4 = graph.GetOrAddEdge(node2, node3, "edge 4"); + + Assert.AreEqual(edge, edge); + Assert.AreNotEqual(edge, edge2); + Assert.AreNotEqual(edge, edge3); + Assert.AreNotEqual(edge, edge4); + Assert.AreEqual(edge.GetHashCode(), edge.GetHashCode()); + Assert.AreNotEqual(edge.GetHashCode(), edge2.GetHashCode()); + Assert.AreNotEqual(edge.GetHashCode(), edge3.GetHashCode()); + Assert.AreNotEqual(edge.GetHashCode(), edge4.GetHashCode()); + } - [Test()] - public void TestCreateNestedStructures() - { - // Documentation: - // Subgraphs are an important construct in Cgraph.They are intended for organizing subsets of - // graph objects and can be used interchangeably with top - level graphs in almost all Cgraph - // functions. A subgraph may contain any nodes or edges of its parent. (When an edge is - // inserted in a subgraph, its nodes are also implicitly inserted if necessary.Similarly, - // insertion of a node or edge automatically implies insertion in all containing subgraphs up - // to the root.) Subgraphs of a graph form a hierarchy(a tree).Cgraph has functions to - // create, search, and iterate over subgraphs. - - // Conclusion: the hierarchical tree structure is maintained in a sane way across all - // operations we can do w.r.t. subgraphs. - - - // If a node is created in a subgraph, it should also be contained in all supergraphs - RootGraph graph = Utils.CreateUniqueTestGraph(); - SubGraph supergraph = graph.GetOrAddSubgraph("level 1"); - string subgraphname = "level 2"; - SubGraph subgraph = supergraph.GetOrAddSubgraph(subgraphname); - string nodename = "test node"; - Node node = subgraph.GetOrAddNode(nodename); - - // Node must be contained in super graph - Assert.True(node.MyRootGraph.Equals(graph)); - Assert.True(supergraph.Contains(node)); - Assert.True(supergraph.Nodes().Contains(node)); - Assert.NotNull(supergraph.GetNode(nodename)); - // Node must be contained in root graph - Assert.True(graph.Contains(node)); - Assert.True(graph.Nodes().Contains(node)); - Assert.NotNull(graph.GetNode(nodename)); - - // Subgraph must be contained in super graph - Assert.True(supergraph.Contains(subgraph)); - Assert.True(supergraph.Descendants().Contains(subgraph)); - Assert.NotNull(supergraph.GetSubgraph(subgraphname)); - // Subgraph must be contained in root graph - Assert.True(graph.Contains(subgraph)); - Assert.True(graph.Descendants().Contains(subgraph)); - // Subgraph cannot be obtained in the following way: - //graph.GetSubgraph(subgraphname) - Assert.Null(graph.GetSubgraph(subgraphname)); - // Use a utility function instead: - Assert.NotNull(graph.GetDescendantByName(subgraphname)); - } + [Test()] + public void TestCreateNestedStructures() + { + // Documentation: + // Subgraphs are an important construct in Cgraph.They are intended for organizing subsets of + // graph objects and can be used interchangeably with top - level graphs in almost all Cgraph + // functions. A subgraph may contain any nodes or edges of its parent. (When an edge is + // inserted in a subgraph, its nodes are also implicitly inserted if necessary.Similarly, + // insertion of a node or edge automatically implies insertion in all containing subgraphs up + // to the root.) Subgraphs of a graph form a hierarchy(a tree).Cgraph has functions to + // create, search, and iterate over subgraphs. + + // Conclusion: the hierarchical tree structure is maintained in a sane way across all + // operations we can do w.r.t. subgraphs. + + + // If a node is created in a subgraph, it should also be contained in all supergraphs + RootGraph graph = Utils.CreateUniqueTestGraph(); + SubGraph supergraph = graph.GetOrAddSubgraph("level 1"); + string subgraphname = "level 2"; + SubGraph subgraph = supergraph.GetOrAddSubgraph(subgraphname); + string nodename = "test node"; + Node node = subgraph.GetOrAddNode(nodename); + + // Node must be contained in super graph + Assert.True(node.MyRootGraph.Equals(graph)); + Assert.True(supergraph.Contains(node)); + Assert.True(supergraph.Nodes().Contains(node)); + Assert.NotNull(supergraph.GetNode(nodename)); + // Node must be contained in root graph + Assert.True(graph.Contains(node)); + Assert.True(graph.Nodes().Contains(node)); + Assert.NotNull(graph.GetNode(nodename)); + + // Subgraph must be contained in super graph + Assert.True(supergraph.Contains(subgraph)); + Assert.True(supergraph.Descendants().Contains(subgraph)); + Assert.NotNull(supergraph.GetSubgraph(subgraphname)); + // Subgraph must be contained in root graph + Assert.True(graph.Contains(subgraph)); + Assert.True(graph.Descendants().Contains(subgraph)); + // Subgraph cannot be obtained in the following way: + //graph.GetSubgraph(subgraphname) + Assert.Null(graph.GetSubgraph(subgraphname)); + // Use a utility function instead: + Assert.NotNull(graph.GetDescendantByName(subgraphname)); + } - [Test()] - public void TestEdgesInSubgraphs() - { - RootGraph graph = Utils.CreateUniqueTestGraph(); - Node node = graph.GetOrAddNode("node"); - Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); - - SubGraph subgraph = graph.GetOrAddSubgraph("sub graph"); - Node subnode = subgraph.GetOrAddNode("subnode"); - Edge subedge_between_node = subgraph.GetOrAddEdge(node, node, "edge 2"); - Edge subedge_between_subnode = subgraph.GetOrAddEdge(subnode, subnode, "edge 3"); - Edge edge_between_subnode = graph.GetOrAddEdge(subnode, subnode, "edge 4"); - - Assert.True(graph.Contains(edge)); - Assert.True(graph.Contains(subedge_between_node)); - Assert.True(graph.Contains(subedge_between_subnode)); - Assert.True(graph.Contains(edge_between_subnode)); - - Assert.False(subgraph.Contains(edge)); - Assert.True(subgraph.Contains(subedge_between_node)); - Assert.True(subgraph.Contains(subedge_between_subnode)); - Assert.False(subgraph.Contains(edge_between_subnode)); - - // Conclusion: - // Subgraphs can contain edges, independently of their endpoints. - // This affects enumeration as follows: - Assert.AreEqual(2, node.EdgesOut(graph).Count()); - Assert.AreEqual(1, node.EdgesOut(subgraph).Count()); - Assert.AreEqual(2, subnode.EdgesOut(graph).Count()); - Assert.AreEqual(1, subnode.EdgesOut(subgraph).Count()); - } + [Test()] + public void TestEdgesInSubgraphs() + { + RootGraph graph = Utils.CreateUniqueTestGraph(); + Node node = graph.GetOrAddNode("node"); + Edge edge = graph.GetOrAddEdge(node, node, "edge 1"); + + SubGraph subgraph = graph.GetOrAddSubgraph("sub graph"); + Node subnode = subgraph.GetOrAddNode("subnode"); + Edge subedge_between_node = subgraph.GetOrAddEdge(node, node, "edge 2"); + Edge subedge_between_subnode = subgraph.GetOrAddEdge(subnode, subnode, "edge 3"); + Edge edge_between_subnode = graph.GetOrAddEdge(subnode, subnode, "edge 4"); + + Assert.True(graph.Contains(edge)); + Assert.True(graph.Contains(subedge_between_node)); + Assert.True(graph.Contains(subedge_between_subnode)); + Assert.True(graph.Contains(edge_between_subnode)); + + Assert.False(subgraph.Contains(edge)); + Assert.True(subgraph.Contains(subedge_between_node)); + Assert.True(subgraph.Contains(subedge_between_subnode)); + Assert.False(subgraph.Contains(edge_between_subnode)); + + // Conclusion: + // Subgraphs can contain edges, independently of their endpoints. + // This affects enumeration as follows: + Assert.AreEqual(2, node.EdgesOut(graph).Count()); + Assert.AreEqual(1, node.EdgesOut(subgraph).Count()); + Assert.AreEqual(2, subnode.EdgesOut(graph).Count()); + Assert.AreEqual(1, subnode.EdgesOut(subgraph).Count()); + } - [Test()] - public void TestRecursiveSubgraphDeletion() - { - RootGraph graph = Utils.CreateUniqueTestGraph(); - Graph.IntroduceAttribute(graph, "label", ""); - graph.SetAttribute("label", "xx"); - Node node = graph.GetOrAddNode("node"); - SubGraph subgraph = graph.GetOrAddSubgraph("subgraph"); - subgraph.SetAttribute("label", "x"); - Node subnode = subgraph.GetOrAddNode("subnode"); - SubGraph subsubgraph = subgraph.GetOrAddSubgraph("subsubgraph"); - subsubgraph.SetAttribute("label", "x"); - Node subsubnode = subsubgraph.GetOrAddNode("subsubnode"); - - // Now deleting subgraph should also delete subsubgraph - Assert.AreNotEqual(null, subgraph.GetSubgraph("subsubgraph")); - Assert.AreNotEqual(null, graph.GetDescendantByName("subsubgraph")); - subgraph.Delete(); - Assert.AreEqual(null, graph.GetDescendantByName("subsubgraph")); - } + [Test()] + public void TestRecursiveSubgraphDeletion() + { + RootGraph graph = Utils.CreateUniqueTestGraph(); + Graph.IntroduceAttribute(graph, "label", ""); + graph.SetAttribute("label", "xx"); + Node node = graph.GetOrAddNode("node"); + SubGraph subgraph = graph.GetOrAddSubgraph("subgraph"); + subgraph.SetAttribute("label", "x"); + Node subnode = subgraph.GetOrAddNode("subnode"); + SubGraph subsubgraph = subgraph.GetOrAddSubgraph("subsubgraph"); + subsubgraph.SetAttribute("label", "x"); + Node subsubnode = subsubgraph.GetOrAddNode("subsubnode"); + + // Now deleting subgraph should also delete subsubgraph + Assert.AreNotEqual(null, subgraph.GetSubgraph("subsubgraph")); + Assert.AreNotEqual(null, graph.GetDescendantByName("subsubgraph")); + subgraph.Delete(); + Assert.AreEqual(null, graph.GetDescendantByName("subsubgraph")); + } - [Test()] - public void DotOutputConsistency() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); + [Test()] + public void DotOutputConsistency() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - root.ComputeLayout(); - var dotstr = root.ToDotString(); - var root2 = RootGraph.FromDotString(dotstr); - var dotstr2 = root2.ToDotString(); - Assert.AreEqual(dotstr, dotstr2); - } + root.ComputeLayout(); + var dotstr = root.ToDotString(); + var root2 = RootGraph.FromDotString(dotstr); + var dotstr2 = root2.ToDotString(); + Assert.AreEqual(dotstr, dotstr2); } } diff --git a/Rubjerg.Graphviz.Test/CGraphIntegrationTests.cs b/Rubjerg.Graphviz.Test/CGraphIntegrationTests.cs index 5265e7b..351d6b4 100644 --- a/Rubjerg.Graphviz.Test/CGraphIntegrationTests.cs +++ b/Rubjerg.Graphviz.Test/CGraphIntegrationTests.cs @@ -2,151 +2,150 @@ using System.Linq; using NUnit.Framework; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +using static Utils; + +[TestFixture()] +public class CGraphIntegrationTests { - using static Utils; + protected virtual int SizeMultiplier => 1; - [TestFixture()] - public class CGraphIntegrationTests + [TestCase(10, 5)] + public void TestTopologicalEqualsIdentity(int nodes, int degree) { - protected virtual int SizeMultiplier => 1; - - [TestCase(10, 5)] - public void TestTopologicalEqualsIdentity(int nodes, int degree) - { - var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); - Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(root, root, Log)); - } + var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); + Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(root, root, Log)); + } - [TestCase(10, 5)] - public void TestTopologicalEqualsClone(int nodes, int degree) - { - var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); - var clone = root.Clone(root.GetName() + "clone"); - Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(root, clone, Log)); - } + [TestCase(10, 5)] + public void TestTopologicalEqualsClone(int nodes, int degree) + { + var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); + var clone = root.Clone(root.GetName() + "clone"); + Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(root, clone, Log)); + } - [TestCase(10, 5)] - public void TestTopologicalEqualsCloneWithSubgraphs(int nodes, int degree) - { - var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); - var nodeSelection = new HashSet(root.Nodes().Take(nodes / 2)); - var sub = root.AddSubgraphFromNodes("sub", nodeSelection); - - var subNodeCount = sub.Nodes().Count(); - var nodeSelection2 = new HashSet(sub.Nodes().Take(subNodeCount / 2)); - var sub2 = root.AddSubgraphFromNodes("sub2", nodeSelection2); - - var edgeCount = sub2.Edges().Count(); - var edgeSelection = new HashSet(sub.Edges().Take(edgeCount / 2)); - var sub3 = root.AddSubgraphFromEdgeSet("sub3", edgeSelection); - - RootGraph subclone = sub.Clone("subclone"); - Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub, subclone, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, root, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, sub2, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, sub3, Log)); - - RootGraph sub2clone = sub2.Clone("sub2clone"); - Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub2, sub2clone, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, root, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, sub, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, sub3, Log)); - - RootGraph sub3clone = sub3.Clone("sub3clone"); - Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub3, sub3clone, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, root, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, sub, Log)); - Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, sub2, Log)); - } + [TestCase(10, 5)] + public void TestTopologicalEqualsCloneWithSubgraphs(int nodes, int degree) + { + var root = CreateRandomConnectedGraph(nodes * SizeMultiplier, degree); + var nodeSelection = new HashSet(root.Nodes().Take(nodes / 2)); + var sub = root.AddSubgraphFromNodes("sub", nodeSelection); + + var subNodeCount = sub.Nodes().Count(); + var nodeSelection2 = new HashSet(sub.Nodes().Take(subNodeCount / 2)); + var sub2 = root.AddSubgraphFromNodes("sub2", nodeSelection2); + + var edgeCount = sub2.Edges().Count(); + var edgeSelection = new HashSet(sub.Edges().Take(edgeCount / 2)); + var sub3 = root.AddSubgraphFromEdgeSet("sub3", edgeSelection); + + RootGraph subclone = sub.Clone("subclone"); + Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub, subclone, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, root, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, sub2, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub, sub3, Log)); + + RootGraph sub2clone = sub2.Clone("sub2clone"); + Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub2, sub2clone, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, root, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, sub, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub2, sub3, Log)); + + RootGraph sub3clone = sub3.Clone("sub3clone"); + Assert.IsTrue(GraphComparer.CheckTopologicallyEquals(sub3, sub3clone, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, root, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, sub, Log)); + Assert.IsFalse(GraphComparer.CheckTopologicallyEquals(sub3, sub2, Log)); + } - /// - /// This test fails if the locking doesn't work, and the GC runs async. - /// - [TestCase(100, 10000)] - public void TestGetNode(int initial_graphs, int get_attempts) + /// + /// This test fails if the locking doesn't work, and the GC runs async. + /// + [TestCase(100, 10000)] + public void TestGetNode(int initial_graphs, int get_attempts) + { + for (int j = 0; j < initial_graphs; j++) { - for (int j = 0; j < initial_graphs; j++) + var pre = CreateUniqueTestGraph(); + for (int i = 0; i < 10; i++) { - var pre = CreateUniqueTestGraph(); - for (int i = 0; i < 10; i++) - { - _ = pre.GetOrAddNode(i.ToString()); - } + _ = pre.GetOrAddNode(i.ToString()); } + } - var root = CreateUniqueTestGraph(); - _ = root.GetOrAddNode("node1"); - for (int i = 0; i < get_attempts * SizeMultiplier; i++) - { - var node = root.GetNode("node1"); - Assert.IsNotNull(node); - } + var root = CreateUniqueTestGraph(); + _ = root.GetOrAddNode("node1"); + for (int i = 0; i < get_attempts * SizeMultiplier; i++) + { + var node = root.GetNode("node1"); + Assert.IsNotNull(node); } + } - [TestCase(500, 10)] - public void TestAddNode(int nodes, int degree) + [TestCase(500, 10)] + public void TestAddNode(int nodes, int degree) + { + int initcount = nodes * SizeMultiplier; + var root = CreateRandomConnectedGraph(initcount, degree); + int addcount = nodes * SizeMultiplier; + var watch = System.Diagnostics.Stopwatch.StartNew(); + for (int i = initcount; i < initcount + addcount; i++) { - int initcount = nodes * SizeMultiplier; - var root = CreateRandomConnectedGraph(initcount, degree); - int addcount = nodes * SizeMultiplier; - var watch = System.Diagnostics.Stopwatch.StartNew(); - for (int i = initcount; i < initcount + addcount; i++) - { - _ = root.GetOrAddNode(i.ToString()); - } - watch.Stop(); - var elapsedms = watch.ElapsedMilliseconds; - Log($"Elapsed ms: {elapsedms}"); - Assert.AreEqual(initcount + addcount, root.Nodes().Count()); + _ = root.GetOrAddNode(i.ToString()); } + watch.Stop(); + var elapsedms = watch.ElapsedMilliseconds; + Log($"Elapsed ms: {elapsedms}"); + Assert.AreEqual(initcount + addcount, root.Nodes().Count()); + } - [TestCase(500, 10)] - public void TestDeleteNode(int nodes, int degree) + [TestCase(500, 10)] + public void TestDeleteNode(int nodes, int degree) + { + int initcount = nodes * 2 * SizeMultiplier; + var root = CreateRandomConnectedGraph(initcount, degree); + int delcount = nodes * SizeMultiplier; + var watch = System.Diagnostics.Stopwatch.StartNew(); + for (int i = initcount - delcount; i < initcount; i++) { - int initcount = nodes * 2 * SizeMultiplier; - var root = CreateRandomConnectedGraph(initcount, degree); - int delcount = nodes * SizeMultiplier; - var watch = System.Diagnostics.Stopwatch.StartNew(); - for (int i = initcount - delcount; i < initcount; i++) - { - var node = root.GetOrAddNode(i.ToString()); - root.Delete(node); - } - watch.Stop(); - var elapsedms = watch.ElapsedMilliseconds; - Log($"Elapsed ms: {elapsedms}"); - Assert.AreEqual(initcount - delcount, root.Nodes().Count()); + var node = root.GetOrAddNode(i.ToString()); + root.Delete(node); } + watch.Stop(); + var elapsedms = watch.ElapsedMilliseconds; + Log($"Elapsed ms: {elapsedms}"); + Assert.AreEqual(initcount - delcount, root.Nodes().Count()); + } - [TestCase(100, 10)] - public void TestBFS(int nodes, int degree) + [TestCase(100, 10)] + public void TestBFS(int nodes, int degree) + { + int initcount = nodes * SizeMultiplier; + var root = CreateRandomConnectedGraph(initcount, degree); + var watch = System.Diagnostics.Stopwatch.StartNew(); + + var start = root.GetOrAddNode(0.ToString()); + var visited = new HashSet(); + var front = new Queue(); + front.Enqueue(start); + while (front.Any()) { - int initcount = nodes * SizeMultiplier; - var root = CreateRandomConnectedGraph(initcount, degree); - var watch = System.Diagnostics.Stopwatch.StartNew(); - - var start = root.GetOrAddNode(0.ToString()); - var visited = new HashSet(); - var front = new Queue(); - front.Enqueue(start); - while (front.Any()) + Node current = front.Dequeue(); + foreach (var n in current.NeighborsOut()) { - Node current = front.Dequeue(); - foreach (var n in current.NeighborsOut()) + if (!visited.Contains(n)) { - if (!visited.Contains(n)) - { - _ = visited.Add(n); - front.Enqueue(n); - } + _ = visited.Add(n); + front.Enqueue(n); } } - - watch.Stop(); - var elapsedms = watch.ElapsedMilliseconds; - Log($"Elapsed ms: {elapsedms}"); - Assert.AreEqual(initcount, visited.Count); } + + watch.Stop(); + var elapsedms = watch.ElapsedMilliseconds; + Log($"Elapsed ms: {elapsedms}"); + Assert.AreEqual(initcount, visited.Count); } } diff --git a/Rubjerg.Graphviz.Test/CGraphStressTests.cs b/Rubjerg.Graphviz.Test/CGraphStressTests.cs index e1b0ae0..913a7c1 100644 --- a/Rubjerg.Graphviz.Test/CGraphStressTests.cs +++ b/Rubjerg.Graphviz.Test/CGraphStressTests.cs @@ -1,11 +1,10 @@ using NUnit.Framework; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +[Category("Slow")] +public class CGraphStressTests : CGraphIntegrationTests { - [TestFixture()] - [Category("Slow")] - public class CGraphStressTests : CGraphIntegrationTests - { - protected override int SizeMultiplier => 100; - } + protected override int SizeMultiplier => 100; } diff --git a/Rubjerg.Graphviz.Test/OldTutorial.cs b/Rubjerg.Graphviz.Test/OldTutorial.cs index 70c137e..18d1523 100644 --- a/Rubjerg.Graphviz.Test/OldTutorial.cs +++ b/Rubjerg.Graphviz.Test/OldTutorial.cs @@ -4,184 +4,183 @@ #pragma warning disable CS0618 // Type or member is obsolete -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +/// +/// This used to be the tutorial, and this still exists for test coverage of the old code that deals with +/// in-place layout computation. +/// +[TestFixture()] +public class OldTutorial { - /// - /// This used to be the tutorial, and this still exists for test coverage of the old code that deals with - /// in-place layout computation. - /// - [TestFixture()] - public class OldTutorial + [Test, Order(1)] + public void GraphConstruction() { - [Test, Order(1)] - public void GraphConstruction() - { - // You can programmatically construct graphs as follows - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); - - // The node names are unique identifiers within a graph in Graphviz - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // The edge name is only unique between two nodes - Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); - Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); - Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); - - // We can attach attributes to nodes, edges and graphs to store information and instruct - // graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, - // or edges) have the same attributes. The attributes first have to be introduced for a - // certain kind, before we can use it. - Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); - nodeA.SetAttribute("my attribute", "othervalue"); - - // Attributes are introduced per kind (Node, Edge, Graph) per root graph. - // So to be able to use "my attribute" on edges, we first have to introduce it as well. - Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); - edgeAB.SetAttribute("my attribute", "othervalue"); - - // To introduce and set an attribute at the same time, there are convenience wrappers - edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - - // Some attributes - like "label" - accept HTML strings as value - // To tell graphviz that a string should be interpreted as HTML use the designated methods - Node.IntroduceAttribute(root, "label", "defaultlabel"); - nodeB.SetAttributeHtml("label", "Some HTML string"); - - // We can simply export this graph to a text file in dot format - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - } - - [Test, Order(2)] - public void Layouting() - { - // If we have a given dot file (in this case the one we generated above), we can also read it back in - RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // Let's have graphviz compute a dot layout for us - root.ComputeLayout(); - - // We can export this to svg - root.RenderToFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg", "svg"); - - // Or programatically read out the layout attributes - Node nodeA = root.GetNode("A"); - PointF position = nodeA.GetPosition(); - Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+}", position.ToString()); - - // Like a bounding box of an object - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString()); - - // Or splines between nodes - Node nodeB = root.GetNode("B"); - Edge edge = root.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); - string splineString = string.Join(", ", spline.Select(p => p.ToString())); - string expectedSplinePattern = - @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}," - + @" {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; - Utils.AssertPattern(expectedSplinePattern, splineString); - - GraphvizLabel nodeLabel = nodeA.GetLabel(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", - nodeLabel.BoundingBox().ToString()); - Utils.AssertPattern(@"Times-Roman", nodeLabel.FontName().ToString()); - - // Once all layout information is obtained from the graph, the resources should be - // reclaimed. To do this, the application should call the cleanup routine associated - // with the layout algorithm used to draw the graph. This is done by a call to - // FreeLayout(). A given graph can be laid out multiple times. The application, however, - // must clean up the earlier layout's information with a call to FreeLayout before - // invoking a new layout function. - root.FreeLayout(); - - // We can use layout engines other than dot by explicitly passing the engine we want - root.ComputeLayout(LayoutEngines.Neato); - root.RenderToFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", "svg"); - } - - [Test, Order(3)] - public void Clusters() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // When a subgraph name is prefixed with cluster, - // the dot layout engine will render it as a box around the containing nodes. - SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); - cluster1.AddExisting(nodeB); - cluster1.AddExisting(nodeC); - SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); - cluster2.AddExisting(nodeD); - - // COMPOUND EDGES - // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this - // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it - // at the border of the cluster. We provide convenience methods for this. - // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". - Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail - // The boolean indicates whether the dummy node should take up any space. When you pass - // false and you have a lot of edges, the edges may start to overlap a lot. - _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); - _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); - _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); - - root.ComputeLayout(); - - SubGraph cluster = root.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = root.GetBoundingBox(); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", clusterbox.ToString()); - Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", rootgraphbox.ToString()); - } - - [Test, Order(4)] - public void Records() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); - Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - - root.ComputeLayout(); - - // The order of the list matches the order in which the labels occur in the label string above. - var rects = nodeA.GetRecordRectangles().ToList(); - Assert.That(rects.Count, Is.EqualTo(9)); - } - - [Test, Order(5)] - public void StringEscaping() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); - Node.IntroduceAttribute(root, "label", "\\N"); - Node nodeA = root.GetOrAddNode("A"); - - // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for you to do just that. - nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - - root.ComputeLayout(); - - // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. - // This can be troubling if you have an externally defined ID for such a port. - // We provide a function that maps strings to valid portnames. - var somePortId = "port id with :| special characters"; - var validPortName = Edge.ConvertUidToPortName(somePortId); - Node nodeB = root.GetOrAddNode("B"); - nodeB.SafeSetAttribute("shape", "record", ""); - nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); - - // The function makes sure different strings don't accidentally map onto the same portname - Assert.That(Edge.ConvertUidToPortName(":"), Is.Not.EqualTo(Edge.ConvertUidToPortName("|"))); - } + // You can programmatically construct graphs as follows + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); + + // The node names are unique identifiers within a graph in Graphviz + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // The edge name is only unique between two nodes + Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); + Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); + Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); + + // We can attach attributes to nodes, edges and graphs to store information and instruct + // graphviz by specifying layout parameters. At the moment we only support string + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, + // or edges) have the same attributes. The attributes first have to be introduced for a + // certain kind, before we can use it. + Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); + nodeA.SetAttribute("my attribute", "othervalue"); + + // Attributes are introduced per kind (Node, Edge, Graph) per root graph. + // So to be able to use "my attribute" on edges, we first have to introduce it as well. + Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); + edgeAB.SetAttribute("my attribute", "othervalue"); + + // To introduce and set an attribute at the same time, there are convenience wrappers + edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); + + // Some attributes - like "label" - accept HTML strings as value + // To tell graphviz that a string should be interpreted as HTML use the designated methods + Node.IntroduceAttribute(root, "label", "defaultlabel"); + nodeB.SetAttributeHtml("label", "Some HTML string"); + + // We can simply export this graph to a text file in dot format + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + } + + [Test, Order(2)] + public void Layouting() + { + // If we have a given dot file (in this case the one we generated above), we can also read it back in + RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // Let's have graphviz compute a dot layout for us + root.ComputeLayout(); + + // We can export this to svg + root.RenderToFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg", "svg"); + + // Or programatically read out the layout attributes + Node nodeA = root.GetNode("A"); + PointF position = nodeA.GetPosition(); + Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+}", position.ToString()); + + // Like a bounding box of an object + RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString()); + + // Or splines between nodes + Node nodeB = root.GetNode("B"); + Edge edge = root.GetEdge(nodeA, nodeB, "Some edge name"); + PointF[] spline = edge.GetFirstSpline(); + string splineString = string.Join(", ", spline.Select(p => p.ToString())); + string expectedSplinePattern = + @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}," + + @" {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; + Utils.AssertPattern(expectedSplinePattern, splineString); + + GraphvizLabel nodeLabel = nodeA.GetLabel(); + Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", + nodeLabel.BoundingBox().ToString()); + Utils.AssertPattern(@"Times-Roman", nodeLabel.FontName().ToString()); + + // Once all layout information is obtained from the graph, the resources should be + // reclaimed. To do this, the application should call the cleanup routine associated + // with the layout algorithm used to draw the graph. This is done by a call to + // FreeLayout(). A given graph can be laid out multiple times. The application, however, + // must clean up the earlier layout's information with a call to FreeLayout before + // invoking a new layout function. + root.FreeLayout(); + + // We can use layout engines other than dot by explicitly passing the engine we want + root.ComputeLayout(LayoutEngines.Neato); + root.RenderToFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", "svg"); + } + + [Test, Order(3)] + public void Clusters() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // When a subgraph name is prefixed with cluster, + // the dot layout engine will render it as a box around the containing nodes. + SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); + cluster1.AddExisting(nodeB); + cluster1.AddExisting(nodeC); + SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); + cluster2.AddExisting(nodeD); + + // COMPOUND EDGES + // Graphviz does not really support edges from and to clusters. However, by adding an + // invisible dummynode and setting the ltail or lhead attributes of an edge this + // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it + // at the border of the cluster. We provide convenience methods for this. + // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". + Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail + // The boolean indicates whether the dummy node should take up any space. When you pass + // false and you have a lot of edges, the edges may start to overlap a lot. + _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); + _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); + _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); + + root.ComputeLayout(); + + SubGraph cluster = root.GetSubgraph("cluster_1"); + RectangleF clusterbox = cluster.GetBoundingBox(); + RectangleF rootgraphbox = root.GetBoundingBox(); + Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", clusterbox.ToString()); + Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", rootgraphbox.ToString()); + } + + [Test, Order(4)] + public void Records() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); + Node nodeA = root.GetOrAddNode("A"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + + root.ComputeLayout(); + + // The order of the list matches the order in which the labels occur in the label string above. + var rects = nodeA.GetRecordRectangles().ToList(); + Assert.That(rects.Count, Is.EqualTo(9)); + } + + [Test, Order(5)] + public void StringEscaping() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); + Node.IntroduceAttribute(root, "label", "\\N"); + Node nodeA = root.GetOrAddNode("A"); + + // Several characters and character sequences can have special meanings in labels, like \N. + // When you want to have a literal string in a label, we provide a convenience function for you to do just that. + nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); + + root.ComputeLayout(); + + // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. + // This can be troubling if you have an externally defined ID for such a port. + // We provide a function that maps strings to valid portnames. + var somePortId = "port id with :| special characters"; + var validPortName = Edge.ConvertUidToPortName(somePortId); + Node nodeB = root.GetOrAddNode("B"); + nodeB.SafeSetAttribute("shape", "record", ""); + nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); + + // The function makes sure different strings don't accidentally map onto the same portname + Assert.That(Edge.ConvertUidToPortName(":"), Is.Not.EqualTo(Edge.ConvertUidToPortName("|"))); } } diff --git a/Rubjerg.Graphviz.Test/Reproductions.cs b/Rubjerg.Graphviz.Test/Reproductions.cs index fe81f9f..6cf8797 100644 --- a/Rubjerg.Graphviz.Test/Reproductions.cs +++ b/Rubjerg.Graphviz.Test/Reproductions.cs @@ -3,111 +3,106 @@ using System.IO; using System.Linq; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +/// +/// Test various scenarios that have caused problems in the past. +/// +[TestFixture()] +public class Reproductions { - /// - /// Test various scenarios that have caused problems in the past. - /// - [TestFixture()] - public class Reproductions + private string _testDir; + + [SetUp] + public void SetUp() + { + // Store the test directory. + _testDir = TestContext.CurrentContext.TestDirectory; + } + + [Test()] + [TestCase("Times-Roman", 7, 0.01)] + [TestCase("Times-Roman", 7, 0.5)] + public void TestRecordShapeAlignment(string fontname, double fontsize, double margin) + { + RootGraph root = Utils.CreateUniqueTestGraph(); + // Margin between label and node boundary in inches + Node.IntroduceAttribute(root, "margin", margin.ToString(CultureInfo.InvariantCulture)); + Node.IntroduceAttribute(root, "fontsize", fontsize.ToString(CultureInfo.InvariantCulture)); + Node.IntroduceAttribute(root, "fontname", fontname); + + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "{20 VH|{1|2}}", ""); + + //TestContext.Write(root.ToDotString()); + root.ComputeLayout(); + //TestContext.Write(root.ToDotString()); + + var rects = nodeA.GetRecordRectangles().ToList(); + Assert.That(rects[0].Right, Is.EqualTo(rects[2].Right)); + } + + // This test only failed when running in isolation + [Test()] + public void MissingLabelRepro() + { + var graph = RootGraph.FromDotFile($"{_testDir}/missing-label-repro.dot"); + graph.ComputeLayout(); + graph.ToSvgFile($"{_testDir}/test.svg"); + string svgString = File.ReadAllText($"{_testDir}/test.svg"); + Assert.IsTrue(svgString.Contains(">OpenNode")); + } + + [Test()] + public void StackOverflowRepro() + { + var graph = RootGraph.FromDotFile($"{_testDir}/stackoverflow-repro.dot"); + graph.ComputeLayout(); + } + + [Test()] + public void TestFromDotFile() + { + _ = RootGraph.FromDotFile($"{_testDir}/missing-label-repro.dot"); + } + + [Test()] + public void TestDotNewlines() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + + var dotString = root.ToDotString(); + Assert.IsFalse(dotString.Contains("\r")); + } + + + [Test()] + public void TestDotNewlines2() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + + var xdotGraph = root.CreateLayout(); + var xNodeA = xdotGraph.GetNode("A"); + var ldraw = xNodeA.GetAttribute("_ldraw_"); + Assert.IsFalse(ldraw.Contains("\n")); + Assert.IsFalse(ldraw.Contains("\r")); + Assert.IsFalse(ldraw.Contains("\\")); + } + + [Test()] + public void TestDotNewlines3() { - private string _testDir; - - [SetUp] - public void SetUp() - { - // Store the test directory. - _testDir = TestContext.CurrentContext.TestDirectory; - } - - /// - /// This test used to fail: https://gitlab.com/graphviz/graphviz/-/issues/1894 - /// It still fails on github hosted VMs: https://gitlab.com/graphviz/graphviz/-/issues/1905 - /// - [Test()] - [TestCase("Times-Roman", 7, 0.01)] - [TestCase("Times-Roman", 7, 0.5)] - [Category("Flaky")] - public void TestRecordShapeAlignment(string fontname, double fontsize, double margin) - { - RootGraph root = Utils.CreateUniqueTestGraph(); - // Margin between label and node boundary in inches - Node.IntroduceAttribute(root, "margin", margin.ToString(CultureInfo.InvariantCulture)); - Node.IntroduceAttribute(root, "fontsize", fontsize.ToString(CultureInfo.InvariantCulture)); - Node.IntroduceAttribute(root, "fontname", fontname); - - Node nodeA = root.GetOrAddNode("A"); - - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "{20 VH|{1|2}}", ""); - - //TestContext.Write(root.ToDotString()); - root.ComputeLayout(); - //TestContext.Write(root.ToDotString()); - - var rects = nodeA.GetRecordRectangles().ToList(); - Assert.That(rects[0].Right, Is.EqualTo(rects[2].Right)); - } - - // This test only failed when running in isolation - [Test()] - public void MissingLabelRepro() - { - var graph = RootGraph.FromDotFile($"{_testDir}/missing-label-repro.dot"); - graph.ComputeLayout(); - graph.ToSvgFile($"{_testDir}/test.svg"); - string svgString = File.ReadAllText($"{_testDir}/test.svg"); - Assert.IsTrue(svgString.Contains(">OpenNode")); - } - - [Test()] - public void StackOverflowRepro() - { - var graph = RootGraph.FromDotFile($"{_testDir}/stackoverflow-repro.dot"); - graph.ComputeLayout(); - } - - [Test()] - public void TestFromDotFile() - { - _ = RootGraph.FromDotFile($"{_testDir}/missing-label-repro.dot"); - } - - [Test()] - public void TestDotNewlines() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - - var dotString = root.ToDotString(); - Assert.IsFalse(dotString.Contains("\r")); - } - - - [Test()] - public void TestDotNewlines2() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - - var xdotGraph = root.CreateLayout(); - var xNodeA = xdotGraph.GetNode("A"); - var ldraw = xNodeA.GetAttribute("_ldraw_"); - Assert.IsFalse(ldraw.Contains("\n")); - Assert.IsFalse(ldraw.Contains("\r")); - Assert.IsFalse(ldraw.Contains("\\")); - } - - [Test()] - public void TestDotNewlines3() - { - var dotstrCrLf = @" + var dotstrCrLf = @" digraph ""test graph 1"" { graph[_draw_ = ""c 9 -#fffffe00 C 7 -#ffffff P 4 0 0 0 72.25 136.5 72.25 136.5 0 "", bb = ""0,0,136.5,72.25"", @@ -130,14 +125,13 @@ 54.45 0 6.75 1 -7 F 14 11 -Times-Roman c 7 -#000000 T 125.12 30.7 0 6.75 1 -8 F shape = record, width = 1.8958]; }"; - var graph = RootGraph.FromDotString(dotstrCrLf); - var nodeA = graph.GetNode("A"); - var ldraw = nodeA.GetAttribute("_ldraw_"); - - Assert.IsFalse(ldraw.Contains("\n")); - Assert.IsFalse(ldraw.Contains("\r")); - Assert.IsFalse(ldraw.Contains("\\")); - } + var graph = RootGraph.FromDotString(dotstrCrLf); + var nodeA = graph.GetNode("A"); + var ldraw = nodeA.GetAttribute("_ldraw_"); + Assert.IsFalse(ldraw.Contains("\n")); + Assert.IsFalse(ldraw.Contains("\r")); + Assert.IsFalse(ldraw.Contains("\\")); } + } diff --git a/Rubjerg.Graphviz.Test/TestDotLayout.cs b/Rubjerg.Graphviz.Test/TestDotLayout.cs index b8f7557..624043a 100644 --- a/Rubjerg.Graphviz.Test/TestDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestDotLayout.cs @@ -6,303 +6,302 @@ using NUnit.Framework; using static Rubjerg.Graphviz.Test.Utils; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class TestDotLayout { - [TestFixture()] - public class TestDotLayout + private static void CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge) { - private static void CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge) - { - root = CreateUniqueTestGraph(); - root.SetAttribute("label", "g"); - nodeA = root.GetOrAddNode("A"); - nodeA.SetAttribute("shape", "record"); - nodeA.SetAttribute("label", "{a|b}"); - nodeA.SetAttribute("color", "red"); - Node nodeB = root.GetOrAddNode("B"); - edge = root.GetOrAddEdge(nodeA, nodeB, ""); - edge.SetAttribute("label", "e"); - edge.SetAttribute("headlabel", "h"); - edge.SetAttribute("taillabel", "t"); - edge.SetAttribute("dir", "both"); - edge.SetAttribute("arrowtail", "vee"); - edge.SetAttribute("arrowhead", "vee"); - } + root = CreateUniqueTestGraph(); + root.SetAttribute("label", "g"); + nodeA = root.GetOrAddNode("A"); + nodeA.SetAttribute("shape", "record"); + nodeA.SetAttribute("label", "{a|b}"); + nodeA.SetAttribute("color", "red"); + Node nodeB = root.GetOrAddNode("B"); + edge = root.GetOrAddEdge(nodeA, nodeB, ""); + edge.SetAttribute("label", "e"); + edge.SetAttribute("headlabel", "h"); + edge.SetAttribute("taillabel", "t"); + edge.SetAttribute("dir", "both"); + edge.SetAttribute("arrowtail", "vee"); + edge.SetAttribute("arrowhead", "vee"); + } - [Test()] - public void TestLayoutMethodsWithoutLayout() - { - CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); - - Assert.AreEqual(root.GetBoundingBox(), default(RectangleF)); - Assert.AreEqual(root.GetColor(), Color.Black); - Assert.AreEqual(root.GetDrawing().Count, 0); - Assert.AreEqual(root.GetLabelDrawing().Count, 0); - - Assert.AreEqual(nodeA.GetPosition(), default(PointF)); - Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleF)); - Assert.AreEqual(nodeA.GetSize(), default(SizeF)); - Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 0); - Assert.AreEqual(nodeA.GetDrawing().Count, 0); - Assert.AreEqual(nodeA.GetLabelDrawing().Count, 0); - - Assert.AreEqual(edge.GetFirstSpline(), null); - Assert.AreEqual(edge.GetSplines().Count(), 0); - Assert.AreEqual(edge.GetDrawing().Count, 0); - Assert.AreEqual(edge.GetLabelDrawing().Count, 0); - Assert.AreEqual(edge.GetHeadArrowDrawing().Count, 0); - Assert.AreEqual(edge.GetTailArrowDrawing().Count, 0); - Assert.AreEqual(edge.GetHeadLabelDrawing().Count, 0); - Assert.AreEqual(edge.GetTailLabelDrawing().Count, 0); - - //root.ToSvgFile("xxx.svg"); - } + [Test()] + public void TestLayoutMethodsWithoutLayout() + { + CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); + + Assert.AreEqual(root.GetBoundingBox(), default(RectangleF)); + Assert.AreEqual(root.GetColor(), Color.Black); + Assert.AreEqual(root.GetDrawing().Count, 0); + Assert.AreEqual(root.GetLabelDrawing().Count, 0); + + Assert.AreEqual(nodeA.GetPosition(), default(PointF)); + Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleF)); + Assert.AreEqual(nodeA.GetSize(), default(SizeF)); + Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 0); + Assert.AreEqual(nodeA.GetDrawing().Count, 0); + Assert.AreEqual(nodeA.GetLabelDrawing().Count, 0); + + Assert.AreEqual(edge.GetFirstSpline(), null); + Assert.AreEqual(edge.GetSplines().Count(), 0); + Assert.AreEqual(edge.GetDrawing().Count, 0); + Assert.AreEqual(edge.GetLabelDrawing().Count, 0); + Assert.AreEqual(edge.GetHeadArrowDrawing().Count, 0); + Assert.AreEqual(edge.GetTailArrowDrawing().Count, 0); + Assert.AreEqual(edge.GetHeadLabelDrawing().Count, 0); + Assert.AreEqual(edge.GetTailLabelDrawing().Count, 0); + + //root.ToSvgFile("xxx.svg"); + } - [Test()] - public void TestLayoutMethodsWithInProcessLayout() - { - CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); + [Test()] + public void TestLayoutMethodsWithInProcessLayout() + { + CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); + + root.ComputeLayout(); + + Assert.AreEqual(root.GetColor(), Color.Black); + Assert.AreNotEqual(root.GetBoundingBox(), default(RectangleF)); + Assert.AreNotEqual(root.GetDrawing().Count, 0); + Assert.AreNotEqual(root.GetLabelDrawing().Count, 0); + + Assert.AreEqual(nodeA.GetColor(), Color.Red); + Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 2); + Assert.AreNotEqual(nodeA.GetPosition(), default(PointF)); + Assert.AreNotEqual(nodeA.GetBoundingBox(), default(RectangleF)); + Assert.AreNotEqual(nodeA.GetSize(), default(SizeF)); + Assert.AreNotEqual(nodeA.GetDrawing().Count, 0); + Assert.AreNotEqual(nodeA.GetLabelDrawing().Count, 0); + + Assert.AreNotEqual(edge.GetFirstSpline(), null); + Assert.AreNotEqual(edge.GetSplines().Count(), 0); + Assert.AreNotEqual(edge.GetDrawing().Count, 0); + Assert.AreNotEqual(edge.GetLabelDrawing().Count, 0); + Assert.AreNotEqual(edge.GetHeadArrowDrawing().Count, 0); + Assert.AreNotEqual(edge.GetTailArrowDrawing().Count, 0); + Assert.AreNotEqual(edge.GetHeadLabelDrawing().Count, 0); + Assert.AreNotEqual(edge.GetTailLabelDrawing().Count, 0); + } - root.ComputeLayout(); + [Test()] + public void TestLayoutMethodsWithLayout() + { + CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); + + var xroot = root.CreateLayout(); + var xnodeA = xroot.GetNode("A"); + var xnodeB = xroot.GetNode("B"); + Edge xedge = xroot.GetEdge(xnodeA, xnodeB, ""); + + Assert.AreEqual(xroot.GetColor(), Color.Black); + Assert.AreNotEqual(xroot.GetBoundingBox(), default(RectangleF)); + Assert.AreNotEqual(xroot.GetDrawing().Count, 0); + Assert.AreNotEqual(xroot.GetLabelDrawing().Count, 0); + + Assert.AreEqual(xnodeA.GetColor(), Color.Red); + Assert.AreEqual(xnodeA.GetRecordRectangles().Count(), 2); + Assert.AreNotEqual(xnodeA.GetPosition(), default(PointF)); + Assert.AreNotEqual(xnodeA.GetBoundingBox(), default(RectangleF)); + Assert.AreNotEqual(xnodeA.GetSize(), default(SizeF)); + Assert.AreNotEqual(xnodeA.GetDrawing().Count, 0); + Assert.AreNotEqual(xnodeA.GetLabelDrawing().Count, 0); + + Assert.AreNotEqual(xedge.GetFirstSpline(), null); + Assert.AreNotEqual(xedge.GetSplines().Count(), 0); + Assert.AreNotEqual(xedge.GetDrawing().Count, 0); + Assert.AreNotEqual(xedge.GetLabelDrawing().Count, 0); + Assert.AreNotEqual(xedge.GetHeadArrowDrawing().Count, 0); + Assert.AreNotEqual(xedge.GetTailArrowDrawing().Count, 0); + Assert.AreNotEqual(xedge.GetHeadLabelDrawing().Count, 0); + Assert.AreNotEqual(xedge.GetTailLabelDrawing().Count, 0); + } - Assert.AreEqual(root.GetColor(), Color.Black); - Assert.AreNotEqual(root.GetBoundingBox(), default(RectangleF)); - Assert.AreNotEqual(root.GetDrawing().Count, 0); - Assert.AreNotEqual(root.GetLabelDrawing().Count, 0); - - Assert.AreEqual(nodeA.GetColor(), Color.Red); - Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 2); - Assert.AreNotEqual(nodeA.GetPosition(), default(PointF)); - Assert.AreNotEqual(nodeA.GetBoundingBox(), default(RectangleF)); - Assert.AreNotEqual(nodeA.GetSize(), default(SizeF)); - Assert.AreNotEqual(nodeA.GetDrawing().Count, 0); - Assert.AreNotEqual(nodeA.GetLabelDrawing().Count, 0); - - Assert.AreNotEqual(edge.GetFirstSpline(), null); - Assert.AreNotEqual(edge.GetSplines().Count(), 0); - Assert.AreNotEqual(edge.GetDrawing().Count, 0); - Assert.AreNotEqual(edge.GetLabelDrawing().Count, 0); - Assert.AreNotEqual(edge.GetHeadArrowDrawing().Count, 0); - Assert.AreNotEqual(edge.GetTailArrowDrawing().Count, 0); - Assert.AreNotEqual(edge.GetHeadLabelDrawing().Count, 0); - Assert.AreNotEqual(edge.GetTailLabelDrawing().Count, 0); - } + [Test()] + public void TestHtmlLabels() + { + RootGraph root = CreateUniqueTestGraph(); + const string labelKey = "label"; + Node.IntroduceAttribute(root, labelKey, ""); + Graph.IntroduceAttribute(root, labelKey, ""); - [Test()] - public void TestLayoutMethodsWithLayout() - { - CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge); - - var xroot = root.CreateLayout(); - var xnodeA = xroot.GetNode("A"); - var xnodeB = xroot.GetNode("B"); - Edge xedge = xroot.GetEdge(xnodeA, xnodeB, ""); - - Assert.AreEqual(xroot.GetColor(), Color.Black); - Assert.AreNotEqual(xroot.GetBoundingBox(), default(RectangleF)); - Assert.AreNotEqual(xroot.GetDrawing().Count, 0); - Assert.AreNotEqual(xroot.GetLabelDrawing().Count, 0); - - Assert.AreEqual(xnodeA.GetColor(), Color.Red); - Assert.AreEqual(xnodeA.GetRecordRectangles().Count(), 2); - Assert.AreNotEqual(xnodeA.GetPosition(), default(PointF)); - Assert.AreNotEqual(xnodeA.GetBoundingBox(), default(RectangleF)); - Assert.AreNotEqual(xnodeA.GetSize(), default(SizeF)); - Assert.AreNotEqual(xnodeA.GetDrawing().Count, 0); - Assert.AreNotEqual(xnodeA.GetLabelDrawing().Count, 0); - - Assert.AreNotEqual(xedge.GetFirstSpline(), null); - Assert.AreNotEqual(xedge.GetSplines().Count(), 0); - Assert.AreNotEqual(xedge.GetDrawing().Count, 0); - Assert.AreNotEqual(xedge.GetLabelDrawing().Count, 0); - Assert.AreNotEqual(xedge.GetHeadArrowDrawing().Count, 0); - Assert.AreNotEqual(xedge.GetTailArrowDrawing().Count, 0); - Assert.AreNotEqual(xedge.GetHeadLabelDrawing().Count, 0); - Assert.AreNotEqual(xedge.GetTailLabelDrawing().Count, 0); - } + Node n1 = root.GetOrAddNode("1"); + Node n2 = root.GetOrAddNode("2"); + root.SetAttributeHtml(labelKey, ""); - [Test()] - public void TestHtmlLabels() - { - RootGraph root = CreateUniqueTestGraph(); - const string labelKey = "label"; - Node.IntroduceAttribute(root, labelKey, ""); - Graph.IntroduceAttribute(root, labelKey, ""); + n1.SetAttribute(labelKey, "plain 1"); + n2.SetAttributeHtml(labelKey, ""); - Node n1 = root.GetOrAddNode("1"); - Node n2 = root.GetOrAddNode("2"); - root.SetAttributeHtml(labelKey, ""); + var result = root.ToDotString(); - n1.SetAttribute(labelKey, "plain 1"); - n2.SetAttributeHtml(labelKey, ""); + Assert.That(result, Does.Contain("\"plain 1\"")); + AssertContainsHtml(result, ""); + AssertContainsHtml(result, ""); + } - var result = root.ToDotString(); + [Test()] + public void TestHtmlLabelsDefault() + { + RootGraph root = CreateUniqueTestGraph(); + const string labelKey = "label"; + Node.IntroduceAttributeHtml(root, labelKey, ""); - Assert.That(result, Does.Contain("\"plain 1\"")); - AssertContainsHtml(result, ""); - AssertContainsHtml(result, ""); - } + Node n1 = root.GetOrAddNode("1"); + Node n2 = root.GetOrAddNode("2"); + Node n3 = root.GetOrAddNode("3"); - [Test()] - public void TestHtmlLabelsDefault() - { - RootGraph root = CreateUniqueTestGraph(); - const string labelKey = "label"; - Node.IntroduceAttributeHtml(root, labelKey, ""); + n1.SetAttribute(labelKey, "plain 1"); + n2.SetAttributeHtml(labelKey, ""); - Node n1 = root.GetOrAddNode("1"); - Node n2 = root.GetOrAddNode("2"); - Node n3 = root.GetOrAddNode("3"); + var result = root.ToDotString(); - n1.SetAttribute(labelKey, "plain 1"); - n2.SetAttributeHtml(labelKey, ""); + Assert.That(result, Does.Contain("\"plain 1\"")); + AssertContainsHtml(result, ""); + AssertContainsHtml(result, ""); + } - var result = root.ToDotString(); + private static void AssertContainsHtml(string result, string html) + { + // Html labels are not string quoted in dot file + Assert.That(result, Does.Not.Contain($"\"{html}\"")); + Assert.That(result, Does.Not.Contain($"\"<{html}>\"")); + // Htmls labels have additional angel bracket delimeters added + Assert.That(result, Does.Contain($"<{html}>")); + } - Assert.That(result, Does.Contain("\"plain 1\"")); - AssertContainsHtml(result, ""); - AssertContainsHtml(result, ""); - } + [Test()] + public void TestRecordShapeOrder() + { + RootGraph root = CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + + root.ComputeLayout(); + + var rects = nodeA.GetRecordRectangles().ToList(); + + // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates + Utils.AssertOrder(rects, r => (r.Left, -r.Top)); + Assert.That(rects.Count, Is.EqualTo(9)); + } - private static void AssertContainsHtml(string result, string html) + [Test()] + public void TestEmptyRecordShapes() + { + RootGraph root = CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "||||", ""); + + root.ComputeLayout(); + + var rects = nodeA.GetRecordRectangles().ToList(); + Assert.That(rects.Count, Is.EqualTo(5)); + root.ToSvgFile(GetTestFilePath("out.svg")); + } + + [Test()] + [TestCase(true)] + [TestCase(false)] + public void TestPortNameConversion(bool escape) + { + string port1 = ">|<"; + string port2 = "B"; + if (escape) { - // Html labels are not string quoted in dot file - Assert.That(result, Does.Not.Contain($"\"{html}\"")); - Assert.That(result, Does.Not.Contain($"\"<{html}>\"")); - // Htmls labels have additional angel bracket delimeters added - Assert.That(result, Does.Contain($"<{html}>")); + port1 = Edge.ConvertUidToPortName(port1); + port2 = Edge.ConvertUidToPortName(port2); } + string label = $"{{<{port1}>1|<{port2}>2}}"; - [Test()] - public void TestRecordShapeOrder() { RootGraph root = CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); + Node node = root.GetOrAddNode("N"); + node.SafeSetAttribute("shape", "record", ""); + node.SafeSetAttribute("label", label, ""); + Edge edge = root.GetOrAddEdge(node, node, ""); + edge.SafeSetAttribute("tailport", port1 + ":n", ""); + edge.SafeSetAttribute("headport", port2 + ":s", ""); + root.ToDotFile(GetTestFilePath("out.gv")); + } + + { + var root = RootGraph.FromDotFile(GetTestFilePath("out.gv")); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + Node node = root.GetNode("N"); + Assert.That(node.GetAttribute("label"), Is.EqualTo(label)); + Edge edge = root.Edges().First(); + Assert.That(edge.GetAttribute("tailport"), Is.EqualTo(port1 + ":n")); + Assert.That(edge.GetAttribute("headport"), Is.EqualTo(port2 + ":s")); root.ComputeLayout(); + root.ToSvgFile(GetTestFilePath("out.svg")); + root.ToDotFile(GetTestFilePath("out.dot")); - var rects = nodeA.GetRecordRectangles().ToList(); + var rects = node.GetRecordRectangles(); + if (escape) + Assert.That(rects.Count, Is.EqualTo(2)); + else + Assert.That(rects.Count, Is.EqualTo(3)); + } + } - // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates - Utils.AssertOrder(rects, r => (r.Left, -r.Top)); - Assert.That(rects.Count, Is.EqualTo(9)); + [Test()] + [TestCase(true)] + [TestCase(false)] + public void TestLabelEscaping(bool escape) + { + string label1 = "|"; + string label2 = @"\N\n\L"; + string label3 = "3"; + if (escape) + { + label1 = CGraphThing.EscapeLabel(label1); + label2 = CGraphThing.EscapeLabel(label2); } - [Test()] - public void TestEmptyRecordShapes() { RootGraph root = CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "||||", ""); - - root.ComputeLayout(); - - var rects = nodeA.GetRecordRectangles().ToList(); - Assert.That(rects.Count, Is.EqualTo(5)); - root.ToSvgFile(GetTestFilePath("out.svg")); + Node node1 = root.GetOrAddNode("1"); + node1.SafeSetAttribute("shape", "record", ""); + node1.SafeSetAttribute("label", label1, ""); + Node node2 = root.GetOrAddNode("2"); + node2.SafeSetAttribute("label", label2, ""); + Node node3 = root.GetOrAddNode("3"); + node3.SafeSetAttribute("label", label3, ""); + root.ToDotFile(GetTestFilePath("out.gv")); } - [Test()] - [TestCase(true)] - [TestCase(false)] - public void TestPortNameConversion(bool escape) { - string port1 = ">|<"; - string port2 = "B"; - if (escape) - { - port1 = Edge.ConvertUidToPortName(port1); - port2 = Edge.ConvertUidToPortName(port2); - } - string label = $"{{<{port1}>1|<{port2}>2}}"; + var root = RootGraph.FromDotFile(GetTestFilePath("out.gv")); - { - RootGraph root = CreateUniqueTestGraph(); - Node node = root.GetOrAddNode("N"); - node.SafeSetAttribute("shape", "record", ""); - node.SafeSetAttribute("label", label, ""); - Edge edge = root.GetOrAddEdge(node, node, ""); - edge.SafeSetAttribute("tailport", port1 + ":n", ""); - edge.SafeSetAttribute("headport", port2 + ":s", ""); - root.ToDotFile(GetTestFilePath("out.gv")); - } + Node node1 = root.GetNode("1"); + Assert.That(node1.GetAttribute("label"), Is.EqualTo(label1)); + Node node2 = root.GetNode("2"); + Assert.That(node2.GetAttribute("label"), Is.EqualTo(label2)); + Node node3 = root.GetNode("3"); + Assert.That(node3.GetAttribute("label"), Is.EqualTo(label3)); - { - var root = RootGraph.FromDotFile(GetTestFilePath("out.gv")); - - Node node = root.GetNode("N"); - Assert.That(node.GetAttribute("label"), Is.EqualTo(label)); - Edge edge = root.Edges().First(); - Assert.That(edge.GetAttribute("tailport"), Is.EqualTo(port1 + ":n")); - Assert.That(edge.GetAttribute("headport"), Is.EqualTo(port2 + ":s")); - - root.ComputeLayout(); - root.ToSvgFile(GetTestFilePath("out.svg")); - root.ToDotFile(GetTestFilePath("out.dot")); - - var rects = node.GetRecordRectangles(); - if (escape) - Assert.That(rects.Count, Is.EqualTo(2)); - else - Assert.That(rects.Count, Is.EqualTo(3)); - } - } + root.ComputeLayout(); + root.ToSvgFile(GetTestFilePath("out.svg")); + root.ToDotFile(GetTestFilePath("out.dot")); - [Test()] - [TestCase(true)] - [TestCase(false)] - public void TestLabelEscaping(bool escape) - { - string label1 = "|"; - string label2 = @"\N\n\L"; - string label3 = "3"; + var rects = node1.GetRecordRectangles(); if (escape) { - label1 = CGraphThing.EscapeLabel(label1); - label2 = CGraphThing.EscapeLabel(label2); + Assert.That(rects.Count, Is.EqualTo(1)); + Assert.That(node2.GetBoundingBox().Height, Is.EqualTo(node3.GetBoundingBox().Height)); } - - { - RootGraph root = CreateUniqueTestGraph(); - Node node1 = root.GetOrAddNode("1"); - node1.SafeSetAttribute("shape", "record", ""); - node1.SafeSetAttribute("label", label1, ""); - Node node2 = root.GetOrAddNode("2"); - node2.SafeSetAttribute("label", label2, ""); - Node node3 = root.GetOrAddNode("3"); - node3.SafeSetAttribute("label", label3, ""); - root.ToDotFile(GetTestFilePath("out.gv")); - } - + else { - var root = RootGraph.FromDotFile(GetTestFilePath("out.gv")); - - Node node1 = root.GetNode("1"); - Assert.That(node1.GetAttribute("label"), Is.EqualTo(label1)); - Node node2 = root.GetNode("2"); - Assert.That(node2.GetAttribute("label"), Is.EqualTo(label2)); - Node node3 = root.GetNode("3"); - Assert.That(node3.GetAttribute("label"), Is.EqualTo(label3)); - - root.ComputeLayout(); - root.ToSvgFile(GetTestFilePath("out.svg")); - root.ToDotFile(GetTestFilePath("out.dot")); - - var rects = node1.GetRecordRectangles(); - if (escape) - { - Assert.That(rects.Count, Is.EqualTo(1)); - Assert.That(node2.GetBoundingBox().Height, Is.EqualTo(node3.GetBoundingBox().Height)); - } - else - { - Assert.That(rects.Count, Is.EqualTo(2)); - Assert.That(node2.GetBoundingBox().Height, Is.Not.EqualTo(node3.GetBoundingBox().Height)); - } + Assert.That(rects.Count, Is.EqualTo(2)); + Assert.That(node2.GetBoundingBox().Height, Is.Not.EqualTo(node3.GetBoundingBox().Height)); } } } diff --git a/Rubjerg.Graphviz.Test/TestInterop.cs b/Rubjerg.Graphviz.Test/TestInterop.cs index b92439f..a2ebdac 100644 --- a/Rubjerg.Graphviz.Test/TestInterop.cs +++ b/Rubjerg.Graphviz.Test/TestInterop.cs @@ -1,37 +1,36 @@ using NUnit.Framework; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class TestInterop { - [TestFixture()] - public class TestInterop + [Test()] + public void TestMarshaling() { - [Test()] - public void TestMarshaling() - { - Assert.True(echobool(true)); - Assert.False(echobool(false)); - Assert.True(return_true()); - Assert.False(return_false()); + Assert.True(echobool(true)); + Assert.False(echobool(false)); + Assert.True(return_true()); + Assert.False(return_false()); - Assert.AreEqual(0, echoint(0)); - Assert.AreEqual(1, echoint(1)); - Assert.AreEqual(-1, echoint(-1)); - Assert.AreEqual(1, return1()); - Assert.AreEqual(-1, return_1()); + Assert.AreEqual(0, echoint(0)); + Assert.AreEqual(1, echoint(1)); + Assert.AreEqual(-1, echoint(-1)); + Assert.AreEqual(1, return1()); + Assert.AreEqual(-1, return_1()); - Assert.AreEqual(TestEnum.Val1, return_enum1()); - Assert.AreEqual(TestEnum.Val2, return_enum2()); - Assert.AreEqual(TestEnum.Val5, return_enum5()); - Assert.AreEqual(TestEnum.Val1, echo_enum(TestEnum.Val1)); - Assert.AreEqual(TestEnum.Val2, echo_enum(TestEnum.Val2)); - Assert.AreEqual(TestEnum.Val5, echo_enum(TestEnum.Val5)); + Assert.AreEqual(TestEnum.Val1, return_enum1()); + Assert.AreEqual(TestEnum.Val2, return_enum2()); + Assert.AreEqual(TestEnum.Val5, return_enum5()); + Assert.AreEqual(TestEnum.Val1, echo_enum(TestEnum.Val1)); + Assert.AreEqual(TestEnum.Val2, echo_enum(TestEnum.Val2)); + Assert.AreEqual(TestEnum.Val5, echo_enum(TestEnum.Val5)); - Assert.AreEqual("", ReturnEmptyString()); - Assert.AreEqual("hello", ReturnHello()); - Assert.AreEqual("1", EchoString("1")); - Assert.AreEqual("", EchoString("")); - Assert.AreEqual("hello", EchoString("hello")); - } + Assert.AreEqual("", ReturnEmptyString()); + Assert.AreEqual("hello", ReturnHello()); + Assert.AreEqual("1", EchoString("1")); + Assert.AreEqual("", EchoString("")); + Assert.AreEqual("hello", EchoString("hello")); } } diff --git a/Rubjerg.Graphviz.Test/TestXDotLayout.cs b/Rubjerg.Graphviz.Test/TestXDotLayout.cs index dfc08b7..3acd2fd 100644 --- a/Rubjerg.Graphviz.Test/TestXDotLayout.cs +++ b/Rubjerg.Graphviz.Test/TestXDotLayout.cs @@ -2,16 +2,16 @@ using System.Linq; using NUnit.Framework; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class TestXDotLayout { - [TestFixture()] - public class TestXDotLayout + [Test()] + public void TestXDotTranslateFromString() { - [Test()] - public void TestXDotTranslateFromString() - { - // Test case containing all possible operations - var testcase = @" + // Test case containing all possible operations + var testcase = @" E 10 10 5 3 e 20 10 5 3 P 4 30 10 30 15 35 15 35 10 @@ -27,87 +27,86 @@ F 12 5 -Arial S 6 -dashed I 90 10 5 5 8 -image.png "; - var result = XDotParser.ParseXDot(testcase); - Assert.AreEqual(14, result.Count); - - } - - [Test()] - public void TestXDotRecordNode() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - - nodeA.SafeSetAttribute("shape", "record", ""); - // FIXNOW: document that newlines are not supported in record labels - nodeA.SafeSetAttribute("label", "1|{2\n3}", "\\N"); - - var xdotGraph = root.CreateLayout(); - var xNodeA = xdotGraph.GetNode("A"); - var ldraw = xNodeA.GetLabelDrawing(); - Assert.IsTrue(ldraw.OfType().Any(t => t.Value.Text == "23")); - // Even though the attribute still contains the newline - Assert.IsTrue(xNodeA.GetAttribute("label") == "1|{2\n3}"); - Assert.AreEqual(6, ldraw.Count); - } - - [Test()] - public void TestXDotNewLines() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - SubGraph cluster = root.GetOrAddSubgraph("cluster_1"); - cluster.SafeSetAttribute("label", "1\n2", ""); - Node nodeA = cluster.GetOrAddNode("A"); - nodeA.SafeSetAttribute("label", "a\nb", ""); - - var xdotGraph = root.CreateLayout(); - - // New lines result in separate text operations - var xCluster = xdotGraph.GetSubgraph("cluster_1"); - var ldraw = xCluster.GetLabelDrawing(); - Assert.AreEqual(6, ldraw.Count); - - var xNodeA = xdotGraph.GetNode("A"); - ldraw = xNodeA.GetLabelDrawing(); - Assert.AreEqual(6, ldraw.Count); - } - - [Test()] - public void TestRecordShapeOrder() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - - - var xdotGraph = root.CreateLayout(); - - var xNodeA = xdotGraph.GetNode("A"); - var rects = xNodeA.GetRecordRectangles().ToList(); - - // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates - Utils.AssertOrder(rects, r => (r.Left, -r.Top)); - Assert.That(rects.Count, Is.EqualTo(9)); - - // Test xdot translation - var xdotDraw = xdotGraph.GetDrawing(); - } - - [Test()] - public void TestEmptyRecordShapes() - { - RootGraph root = Utils.CreateUniqueTestGraph(); - Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "||||", ""); - - var xdotGraph = root.CreateLayout(); - - var xNodeA = xdotGraph.GetNode("A"); - var rects = xNodeA.GetRecordRectangles().ToList(); - Assert.That(rects.Count, Is.EqualTo(5)); - } + var result = XDotParser.ParseXDot(testcase); + Assert.AreEqual(14, result.Count); + + } + + [Test()] + public void TestXDotRecordNode() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + // FIXNOW: document that newlines are not supported in record labels + nodeA.SafeSetAttribute("label", "1|{2\n3}", "\\N"); + + var xdotGraph = root.CreateLayout(); + var xNodeA = xdotGraph.GetNode("A"); + var ldraw = xNodeA.GetLabelDrawing(); + Assert.IsTrue(ldraw.OfType().Any(t => t.Value.Text == "23")); + // Even though the attribute still contains the newline + Assert.IsTrue(xNodeA.GetAttribute("label") == "1|{2\n3}"); + Assert.AreEqual(6, ldraw.Count); + } + + [Test()] + public void TestXDotNewLines() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + SubGraph cluster = root.GetOrAddSubgraph("cluster_1"); + cluster.SafeSetAttribute("label", "1\n2", ""); + Node nodeA = cluster.GetOrAddNode("A"); + nodeA.SafeSetAttribute("label", "a\nb", ""); + + var xdotGraph = root.CreateLayout(); + + // New lines result in separate text operations + var xCluster = xdotGraph.GetSubgraph("cluster_1"); + var ldraw = xCluster.GetLabelDrawing(); + Assert.AreEqual(6, ldraw.Count); + + var xNodeA = xdotGraph.GetNode("A"); + ldraw = xNodeA.GetLabelDrawing(); + Assert.AreEqual(6, ldraw.Count); + } + + [Test()] + public void TestRecordShapeOrder() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + + + var xdotGraph = root.CreateLayout(); + + var xNodeA = xdotGraph.GetNode("A"); + var rects = xNodeA.GetRecordRectangles().ToList(); + + // Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates + Utils.AssertOrder(rects, r => (r.Left, -r.Top)); + Assert.That(rects.Count, Is.EqualTo(9)); + + // Test xdot translation + var xdotDraw = xdotGraph.GetDrawing(); + } + + [Test()] + public void TestEmptyRecordShapes() + { + RootGraph root = Utils.CreateUniqueTestGraph(); + Node nodeA = root.GetOrAddNode("A"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "||||", ""); + + var xdotGraph = root.CreateLayout(); + + var xNodeA = xdotGraph.GetNode("A"); + var rects = xNodeA.GetRecordRectangles().ToList(); + Assert.That(rects.Count, Is.EqualTo(5)); } } diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 91f879f..6a24f5d 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -2,219 +2,218 @@ using System.Drawing; using System.Linq; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +[TestFixture()] +public class Tutorial { - [TestFixture()] - public class Tutorial + public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; + public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; + public const string SplinePattern = + @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; + + [Test, Order(1)] + public void GraphConstruction() { - public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}"; - public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}"; - public const string SplinePattern = - @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; + // You can programmatically construct graphs as follows + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); + // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, + // except it is recorded and preserved when the graph is written as a file. + + // The node names are unique identifiers within a graph in Graphviz + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + + // The edge name is only unique between two nodes + Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); + Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); + Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); + + // An edge name is optional and omitting it will result in a new nameless edge. + // There can be multiple nameless edges between any two nodes. + Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); + Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); + Assert.AreNotEqual(edgeAB1, edgeAB2); + + // We can attach attributes to nodes, edges and graphs to store information and instruct + // Graphviz by specifying layout parameters. At the moment we only support string + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, + // or edges) have the same attributes. An attribute has to be introduced with a default value + // first for a certain kind, before we can use it. + Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); + nodeA.SetAttribute("my attribute", "othervalue"); + + // Attributes are introduced per kind (Node, Edge, Graph) per root graph. + // So to be able to use "my attribute" on edges, we first have to introduce it as well. + Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); + edgeAB.SetAttribute("my attribute", "othervalue"); + + // To introduce and set an attribute at the same time, there are convenience wrappers + edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); + // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. + edgeBC.SetAttribute("new attr", "value"); + + // Some attributes - like "label" - accept HTML strings as value + // To tell Graphviz that a string should be interpreted as HTML use the designated methods + Node.IntroduceAttribute(root, "label", "defaultlabel"); + nodeB.SetAttributeHtml("label", "Some HTML string"); + + // We can simply export this graph to a text file in dot format + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // A word of advice, Graphviz doesn't play very well with empty strings. + // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + } - [Test, Order(1)] - public void GraphConstruction() + [Test, Order(2)] + public void Layouting() + { + // If we have a given dot file (in this case the one we generated above), we can also read it back in + RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // We can ask Graphviz to compute a layout and render it to svg + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); + + // We can use layout engines other than dot by explicitly passing the engine we want + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); + + // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes + // This will create a copy of our original graph with layout information attached to it in the form + // of attributes. + RootGraph layout = root.CreateLayout(); + + // There are convenience methods available that parse these attributes for us and give + // back the layout information in an accessible form. + Node nodeA = layout.GetNode("A"); + PointF position = nodeA.GetPosition(); + Utils.AssertPattern(PointPattern, position.ToString()); + + RectangleF nodeboundingbox = nodeA.GetBoundingBox(); + Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); + + // Or splines between nodes + Node nodeB = layout.GetNode("B"); + Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); + PointF[] spline = edge.GetFirstSpline(); + string splineString = string.Join(", ", spline.Select(p => p.ToString())); + Utils.AssertPattern(SplinePattern, splineString); + + // If we require detailed drawing information for any object, we can retrieve the so called "xdot" + // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. + var activeColor = Color.Black; + foreach (var op in nodeA.GetDrawing()) { - // You can programmatically construct graphs as follows - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); - // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, - // except it is recorded and preserved when the graph is written as a file. - - // The node names are unique identifiers within a graph in Graphviz - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - - // The edge name is only unique between two nodes - Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); - Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); - Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); - - // An edge name is optional and omitting it will result in a new nameless edge. - // There can be multiple nameless edges between any two nodes. - Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); - Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); - Assert.AreNotEqual(edgeAB1, edgeAB2); - - // We can attach attributes to nodes, edges and graphs to store information and instruct - // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, - // or edges) have the same attributes. An attribute has to be introduced with a default value - // first for a certain kind, before we can use it. - Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); - nodeA.SetAttribute("my attribute", "othervalue"); - - // Attributes are introduced per kind (Node, Edge, Graph) per root graph. - // So to be able to use "my attribute" on edges, we first have to introduce it as well. - Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); - edgeAB.SetAttribute("my attribute", "othervalue"); - - // To introduce and set an attribute at the same time, there are convenience wrappers - edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. - edgeBC.SetAttribute("new attr", "value"); - - // Some attributes - like "label" - accept HTML strings as value - // To tell Graphviz that a string should be interpreted as HTML use the designated methods - Node.IntroduceAttribute(root, "label", "defaultlabel"); - nodeB.SetAttributeHtml("label", "Some HTML string"); - - // We can simply export this graph to a text file in dot format - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // A word of advice, Graphviz doesn't play very well with empty strings. - // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + if (op is XDotOp.FillColor { Value: string htmlColor }) + { + activeColor = ColorTranslator.FromHtml(htmlColor); + } + else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) + { + var boundingBox = filledEllipse.ToRectangleF(); + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + } + // Handle any xdot operation you require } - [Test, Order(2)] - public void Layouting() + var activeFont = XDotFont.Default; + foreach (var op in nodeA.GetDrawing()) { - // If we have a given dot file (in this case the one we generated above), we can also read it back in - RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // We can ask Graphviz to compute a layout and render it to svg - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - - // We can use layout engines other than dot by explicitly passing the engine we want - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - - // Or we can ask Graphviz to compute the layout and programatically read out the layout attributes - // This will create a copy of our original graph with layout information attached to it in the form - // of attributes. - RootGraph layout = root.CreateLayout(); - - // There are convenience methods available that parse these attributes for us and give - // back the layout information in an accessible form. - Node nodeA = layout.GetNode("A"); - PointF position = nodeA.GetPosition(); - Utils.AssertPattern(PointPattern, position.ToString()); - - RectangleF nodeboundingbox = nodeA.GetBoundingBox(); - Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); - - // Or splines between nodes - Node nodeB = layout.GetNode("B"); - Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name"); - PointF[] spline = edge.GetFirstSpline(); - string splineString = string.Join(", ", spline.Select(p => p.ToString())); - Utils.AssertPattern(SplinePattern, splineString); - - // If we require detailed drawing information for any object, we can retrieve the so called "xdot" - // operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification. - var activeColor = Color.Black; - foreach (var op in nodeA.GetDrawing()) + if (op is XDotOp.Font { Value: var font }) { - if (op is XDotOp.FillColor { Value: string htmlColor }) - { - activeColor = ColorTranslator.FromHtml(htmlColor); - } - else if (op is XDotOp.FilledEllipse { Value: var filledEllipse }) - { - var boundingBox = filledEllipse.ToRectangleF(); - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - } - // Handle any xdot operation you require + activeFont = font; + Utils.AssertPattern(@"Times-Roman", font.Name); } - - var activeFont = XDotFont.Default; - foreach (var op in nodeA.GetDrawing()) + else if (op is XDotOp.Text { Value: var text }) { - if (op is XDotOp.Font { Value: var font }) - { - activeFont = font; - Utils.AssertPattern(@"Times-Roman", font.Name); - } - else if (op is XDotOp.Text { Value: var text }) - { - var anchor = text.Anchor(); - Utils.AssertPattern(PointPattern, anchor.ToString()); - var boundingBox = text.TextBoundingBox(activeFont); - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - Assert.AreEqual(text.Text, "A"); - } - // Handle any xdot operation you require + var anchor = text.Anchor(); + Utils.AssertPattern(PointPattern, anchor.ToString()); + var boundingBox = text.TextBoundingBox(activeFont); + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + Assert.AreEqual(text.Text, "A"); } - - // These are just simple examples to showcase the structure of xdot operations. - // In reality the information can be much richer and more complex. + // Handle any xdot operation you require } - [Test, Order(3)] - public void Clusters() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // When a subgraph name is prefixed with cluster, - // the dot layout engine will render it as a box around the containing nodes. - SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); - cluster1.AddExisting(nodeB); - cluster1.AddExisting(nodeC); - SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); - cluster2.AddExisting(nodeD); - - // COMPOUND EDGES - // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this - // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it - // at the border of the cluster. We provide convenience methods for this. - // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". - Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail - // The boolean indicates whether the dummy node should take up any space. When you pass - // false and you have a lot of edges, the edges may start to overlap a lot. - _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); - _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); - _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); - - var layout = root.CreateLayout(); - - SubGraph cluster = layout.GetSubgraph("cluster_1"); - RectangleF clusterbox = cluster.GetBoundingBox(); - RectangleF rootgraphbox = layout.GetBoundingBox(); - Utils.AssertPattern(RectPattern, clusterbox.ToString()); - Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); - } + // These are just simple examples to showcase the structure of xdot operations. + // In reality the information can be much richer and more complex. + } - [Test, Order(4)] - public void Records() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); - Node nodeA = root.GetOrAddNode("A"); - nodeA.SafeSetAttribute("shape", "record", ""); - nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); + [Test, Order(3)] + public void Clusters() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // When a subgraph name is prefixed with cluster, + // the dot layout engine will render it as a box around the containing nodes. + SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); + cluster1.AddExisting(nodeB); + cluster1.AddExisting(nodeC); + SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); + cluster2.AddExisting(nodeD); + + // COMPOUND EDGES + // Graphviz does not really support edges from and to clusters. However, by adding an + // invisible dummynode and setting the ltail or lhead attributes of an edge this + // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it + // at the border of the cluster. We provide convenience methods for this. + // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". + Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail + // The boolean indicates whether the dummy node should take up any space. When you pass + // false and you have a lot of edges, the edges may start to overlap a lot. + _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); + _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); + _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); + + var layout = root.CreateLayout(); + + SubGraph cluster = layout.GetSubgraph("cluster_1"); + RectangleF clusterbox = cluster.GetBoundingBox(); + RectangleF rootgraphbox = layout.GetBoundingBox(); + Utils.AssertPattern(RectPattern, clusterbox.ToString()); + Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); + } - var layout = root.CreateLayout(); + [Test, Order(4)] + public void Records() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); + Node nodeA = root.GetOrAddNode("A"); + nodeA.SafeSetAttribute("shape", "record", ""); + nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N"); - // The order of the list matches the order in which the labels occur in the label string above. - var rects = layout.GetNode("A").GetRecordRectangles().ToList(); - Assert.AreEqual(9, rects.Count); - } + var layout = root.CreateLayout(); - [Test, Order(5)] - public void StringEscaping() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); - Node.IntroduceAttribute(root, "label", "\\N"); - Node nodeA = root.GetOrAddNode("A"); - - // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for you to do just that. - nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - - // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. - // This can be troubling if you have an externally defined ID for such a port. - // We provide a function that maps strings to valid portnames. - var somePortId = "port id with :| special characters"; - var validPortName = Edge.ConvertUidToPortName(somePortId); - Node nodeB = root.GetOrAddNode("B"); - nodeB.SafeSetAttribute("shape", "record", ""); - nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); - - // The conversion function makes sure different strings don't accidentally map onto the same portname - Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); - } + // The order of the list matches the order in which the labels occur in the label string above. + var rects = layout.GetNode("A").GetRecordRectangles().ToList(); + Assert.AreEqual(9, rects.Count); + } + + [Test, Order(5)] + public void StringEscaping() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); + Node.IntroduceAttribute(root, "label", "\\N"); + Node nodeA = root.GetOrAddNode("A"); + + // Several characters and character sequences can have special meanings in labels, like \N. + // When you want to have a literal string in a label, we provide a convenience function for you to do just that. + nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); + + // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. + // This can be troubling if you have an externally defined ID for such a port. + // We provide a function that maps strings to valid portnames. + var somePortId = "port id with :| special characters"; + var validPortName = Edge.ConvertUidToPortName(somePortId); + Node nodeB = root.GetOrAddNode("B"); + nodeB.SafeSetAttribute("shape", "record", ""); + nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N"); + + // The conversion function makes sure different strings don't accidentally map onto the same portname + Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); } } diff --git a/Rubjerg.Graphviz.Test/Utils.cs b/Rubjerg.Graphviz.Test/Utils.cs index aa06092..19397bf 100644 --- a/Rubjerg.Graphviz.Test/Utils.cs +++ b/Rubjerg.Graphviz.Test/Utils.cs @@ -3,92 +3,91 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace Rubjerg.Graphviz.Test +namespace Rubjerg.Graphviz.Test; + +public static class Utils { - public static class Utils + private static readonly Random _rand = new Random(); + + internal static int _rootGraphCounter = 0; + public static RootGraph CreateUniqueTestGraph() { - private static readonly Random _rand = new Random(); + _rootGraphCounter += 1; + return RootGraph.CreateNew(GraphType.Directed, "test graph " + _rootGraphCounter.ToString()); + } - internal static int _rootGraphCounter = 0; - public static RootGraph CreateUniqueTestGraph() - { - _rootGraphCounter += 1; - return RootGraph.CreateNew(GraphType.Directed, "test graph " + _rootGraphCounter.ToString()); - } + public static string GetTestFilePath(string fileName) + { + return $"{TestContext.CurrentContext.TestDirectory}/{fileName}"; + } + + public static RootGraph CreateRandomConnectedGraph(int size, double out_degree) + { + RootGraph root = CreateUniqueTestGraph(); - public static string GetTestFilePath(string fileName) + // First generate a star of requested size + Node centernode = root.GetOrAddNode(0.ToString()); + for (int i = 1; i < size; i++) { - return $"{TestContext.CurrentContext.TestDirectory}/{fileName}"; + var node = root.GetOrAddNode(i.ToString()); + _ = root.GetOrAddEdge(centernode, node, $"{0} to {i}"); } - public static RootGraph CreateRandomConnectedGraph(int size, double out_degree) + // For each node pick requested number of random neighbors + for (int i = 0; i < size; i++) { - RootGraph root = CreateUniqueTestGraph(); - - // First generate a star of requested size - Node centernode = root.GetOrAddNode(0.ToString()); - for (int i = 1; i < size; i++) + // We already have one out edge for each node + for (int x = 0; x < out_degree - 2; x++) { - var node = root.GetOrAddNode(i.ToString()); - _ = root.GetOrAddEdge(centernode, node, $"{0} to {i}"); + var node = root.GetNode(i.ToString()); + int j = _rand.Next(size - 1); + var neighbor = root.GetNode(j.ToString()); + _ = root.GetOrAddEdge(node, neighbor, $"{i} to {j}"); } - - // For each node pick requested number of random neighbors - for (int i = 0; i < size; i++) - { - // We already have one out edge for each node - for (int x = 0; x < out_degree - 2; x++) - { - var node = root.GetNode(i.ToString()); - int j = _rand.Next(size - 1); - var neighbor = root.GetNode(j.ToString()); - _ = root.GetOrAddEdge(node, neighbor, $"{i} to {j}"); - } - } - - return root; } - public static void Log(string message) - { + return root; + } + + public static void Log(string message) + { #if debug - TestContext.WriteLine(message); + TestContext.WriteLine(message); #endif - } + } - public static void AssertPattern(string expectedRegex, string actual) - { - Assert.IsTrue(Regex.IsMatch(actual, expectedRegex)); - } + public static void AssertPattern(string expectedRegex, string actual) + { + Assert.IsTrue(Regex.IsMatch(actual, expectedRegex)); + } - public static void AssertOrder(this IEnumerable source, Func keySelector) - { - Assert.IsTrue(IsOrdered(source, keySelector)); - } + public static void AssertOrder(this IEnumerable source, Func keySelector) + { + Assert.IsTrue(IsOrdered(source, keySelector)); + } - public static bool IsOrdered(this IEnumerable source, Func keySelector) - { - _ = source ?? throw new ArgumentNullException("source"); + public static bool IsOrdered(this IEnumerable source, Func keySelector) + { + _ = source ?? throw new ArgumentNullException("source"); - var comparer = Comparer.Default; - using (var iterator = source.GetEnumerator()) - { - if (!iterator.MoveNext()) - return true; + var comparer = Comparer.Default; + using (var iterator = source.GetEnumerator()) + { + if (!iterator.MoveNext()) + return true; - TKey current = keySelector(iterator.Current); + TKey current = keySelector(iterator.Current); - while (iterator.MoveNext()) - { - TKey next = keySelector(iterator.Current); - if (comparer.Compare(current, next) > 0) - return false; + while (iterator.MoveNext()) + { + TKey next = keySelector(iterator.Current); + if (comparer.Compare(current, next) > 0) + return false; - current = next; - } + current = next; } - - return true; } + + return true; } } diff --git a/Rubjerg.Graphviz.TransitiveTest/Rubjerg.Graphviz.TransitiveTest.csproj b/Rubjerg.Graphviz.TransitiveTest/Rubjerg.Graphviz.TransitiveTest.csproj index 3e4f77e..00c6c76 100644 --- a/Rubjerg.Graphviz.TransitiveTest/Rubjerg.Graphviz.TransitiveTest.csproj +++ b/Rubjerg.Graphviz.TransitiveTest/Rubjerg.Graphviz.TransitiveTest.csproj @@ -9,6 +9,7 @@ Rubjerg ..\packages true + latest diff --git a/Rubjerg.Graphviz.TransitiveTest/TestReference.cs b/Rubjerg.Graphviz.TransitiveTest/TestReference.cs index da9fbdf..8e34e7b 100644 --- a/Rubjerg.Graphviz.TransitiveTest/TestReference.cs +++ b/Rubjerg.Graphviz.TransitiveTest/TestReference.cs @@ -1,15 +1,15 @@ using System.Linq; using NUnit.Framework; -namespace Rubjerg.Graphviz.NugetTest +namespace Rubjerg.Graphviz.NugetTest; + +[TestFixture()] +public class TestReference { - [TestFixture()] - public class TestReference + [Test()] + public void TestReadDotFile() { - [Test()] - public void TestReadDotFile() - { - RootGraph root = RootGraph.FromDotString(@" + RootGraph root = RootGraph.FromDotString(@" digraph test { A; B; @@ -19,12 +19,11 @@ digraph test { A -> B[name = edgename]; } "); - var A = root.GetNode("A"); - Assert.AreEqual(3, A.EdgesOut().Count()); + var A = root.GetNode("A"); + Assert.AreEqual(3, A.EdgesOut().Count()); - var B = root.GetNode("B"); - _ = root.GetOrAddEdge(A, B, ""); - Assert.AreEqual(4, A.EdgesOut().Count()); - } + var B = root.GetNode("B"); + _ = root.GetOrAddEdge(A, B, ""); + Assert.AreEqual(4, A.EdgesOut().Count()); } } diff --git a/Rubjerg.Graphviz/CGraphThing.cs b/Rubjerg.Graphviz/CGraphThing.cs index ee7db89..b0b220b 100644 --- a/Rubjerg.Graphviz/CGraphThing.cs +++ b/Rubjerg.Graphviz/CGraphThing.cs @@ -5,221 +5,220 @@ using System.Drawing; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +// NOTE ABOUT ATTRIBUTES: +// Cgraph assumes that all objects of a given kind(graphs/subgraphs, nodes, or edges) have the same attributes +// - there’s no notion of subtyping within attributes. Information about attributes is stored in data dictionaries. +// Each graph has three (for graphs/subgraphs, nodes, and edges) for which you'll need the predefined constants +// AGRAPH, AGNODE and AGEDGE in calls to create, search and walk these dictionaries. +public abstract class CGraphThing : GraphvizThing { - // NOTE ABOUT ATTRIBUTES: - // Cgraph assumes that all objects of a given kind(graphs/subgraphs, nodes, or edges) have the same attributes - // - there’s no notion of subtyping within attributes. Information about attributes is stored in data dictionaries. - // Each graph has three (for graphs/subgraphs, nodes, and edges) for which you'll need the predefined constants - // AGRAPH, AGNODE and AGEDGE in calls to create, search and walk these dictionaries. - public abstract class CGraphThing : GraphvizThing - { - public readonly RootGraph MyRootGraph; - - /// - /// Argument root may be null. - /// In that case, it is assumed this is a RootGraph, and MyRootGraph is set to `this`. - /// - internal CGraphThing(IntPtr ptr, RootGraph root) : base(ptr) - { - if (root == null) - MyRootGraph = (RootGraph)this; - else - MyRootGraph = root; - } + public readonly RootGraph MyRootGraph; - protected static string NameString(string name) - { - // Because graphviz does not properly export empty strings to dot, this opens a can of worms. - // So we disallow it, and map it onto null. - // Related issue: https://gitlab.com/graphviz/graphviz/-/issues/1887 - return name == string.Empty ? null : name; - } + /// + /// Argument root may be null. + /// In that case, it is assumed this is a RootGraph, and MyRootGraph is set to `this`. + /// + internal CGraphThing(IntPtr ptr, RootGraph root) : base(ptr) + { + if (root == null) + MyRootGraph = (RootGraph)this; + else + MyRootGraph = root; + } - /// - /// Identifier for this object. Used to distinghuish multi edges. - /// Edges can be nameless, and in that case this method returns null. - /// - public string GetName() - { - return NameString(Rjagnameof(_ptr)); - } + protected static string NameString(string name) + { + // Because graphviz does not properly export empty strings to dot, this opens a can of worms. + // So we disallow it, and map it onto null. + // Related issue: https://gitlab.com/graphviz/graphviz/-/issues/1887 + return name == string.Empty ? null : name; + } - public bool HasAttribute(string name) - { - return !new[] { null, "" }.Contains(GetAttribute(name)); - } + /// + /// Identifier for this object. Used to distinghuish multi edges. + /// Edges can be nameless, and in that case this method returns null. + /// + public string GetName() + { + return NameString(Rjagnameof(_ptr)); + } - /// - /// Set attribute, and introduce it with the given default if it is not introduced yet. - /// - public void SafeSetAttribute(string name, string value, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - Agsafeset(_ptr, name, value, deflt); - } + public bool HasAttribute(string name) + { + return !new[] { null, "" }.Contains(GetAttribute(name)); + } - /// - /// Set attribute, and introduce it with the empty string if it does not exist yet. - /// - public void SetAttribute(string name, string value) - { - Agsafeset(_ptr, name, value, ""); - } + /// + /// Set attribute, and introduce it with the given default if it is not introduced yet. + /// + public void SafeSetAttribute(string name, string value, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + Agsafeset(_ptr, name, value, deflt); + } - /// - /// Get the attribute value for this object, or the default value of the attribute if no explicit value was set. - /// If the attribute was not introduced, return null. - /// - public string GetAttribute(string name) - { - return Agget(_ptr, name); - } + /// + /// Set attribute, and introduce it with the empty string if it does not exist yet. + /// + public void SetAttribute(string name, string value) + { + Agsafeset(_ptr, name, value, ""); + } - /// - /// Get the attribute if it was introduced and contains a non-empty value, otherwise return deflt. - /// - public string SafeGetAttribute(string name, string deflt) - { - if (HasAttribute(name)) - return GetAttribute(name); - return deflt; - } + /// + /// Get the attribute value for this object, or the default value of the attribute if no explicit value was set. + /// If the attribute was not introduced, return null. + /// + public string GetAttribute(string name) + { + return Agget(_ptr, name); + } - public void SetAttributeHtml(string name, string value) - { - AgsetHtml(_ptr, name, value); - } + /// + /// Get the attribute if it was introduced and contains a non-empty value, otherwise return deflt. + /// + public string SafeGetAttribute(string name, string deflt) + { + if (HasAttribute(name)) + return GetAttribute(name); + return deflt; + } - /// - /// Get all attributes as dictionary. - /// - /// - public Dictionary GetAttributes() + public void SetAttributeHtml(string name, string value) + { + AgsetHtml(_ptr, name, value); + } + + /// + /// Get all attributes as dictionary. + /// + /// + public Dictionary GetAttributes() + { + Dictionary attributes = new Dictionary(); + for (int kind = 0; kind < 3; ++kind) { - Dictionary attributes = new Dictionary(); - for (int kind = 0; kind < 3; ++kind) + IntPtr sym = Agnxtattr(MyRootGraph._ptr, kind, IntPtr.Zero); + while (sym != IntPtr.Zero) { - IntPtr sym = Agnxtattr(MyRootGraph._ptr, kind, IntPtr.Zero); - while (sym != IntPtr.Zero) + string key = ImsymKey(sym); + if (HasAttribute(key)) { - string key = ImsymKey(sym); - if (HasAttribute(key)) + string value = GetAttribute(key); + if (!string.IsNullOrEmpty(value)) { - string value = GetAttribute(key); - if (!string.IsNullOrEmpty(value)) - { - attributes[key] = value; - } + attributes[key] = value; } - sym = Agnxtattr(MyRootGraph._ptr, kind, sym); } + sym = Agnxtattr(MyRootGraph._ptr, kind, sym); } - return attributes; - } - - public override string ToString() - { - return $"Name: {GetName()}, root name: {MyRootGraph.GetName()}"; } + return attributes; + } - /// - /// Copy attributes from one cgraph object to another. - /// Throw argument exception if self and destination are not of the same type. - /// Also copies attributes that haven't been introduced in the destination object, - /// unless introduce_new_attrs is false. - /// Return code indicates success or failure. - /// - public int CopyAttributesTo(CGraphThing destination, bool introduce_new_attrs = true) - { - if (!((this is Node && destination is Node) - || (this is Edge && destination is Edge) - || (this is Graph && destination is Graph))) - throw new ArgumentException("Argument must be of the same type as self."); - - // Copying the attributes doesn't work if they have not been introduced in the graph - // Moreover, the copying may just stop at some point if copying of a single attribute fails - if (introduce_new_attrs && MyRootGraph._ptr != destination.MyRootGraph._ptr) - CloneAttributeDeclarations(MyRootGraph._ptr, destination.MyRootGraph._ptr); - - // agcopyattr returns non-zero number on failure. - - // Problems have been observed while copying between edges (of the same graph) - // Hypothesis: are the pointers different in this case? - // Hypothesis 2: does it have something to do with the comment "Do not copy key attribute for edges, - // as this must be distinct." in the graphviz source code? - // Returncode 1 has been observed while copying rootgraphs, while the number of attributes was 0 - Debug.Assert(_ptr != destination._ptr); - int success = Agcopyattr(_ptr, destination._ptr); - // We implement a workaround for copying between edges - if (success != 0) - { - // Fail for unknown failing cases - if (GetType() != typeof(Edge) && GetAttributes().Count != 0) - Debug.Fail("Copying attributes failed"); - - // We work around this doing the following: - var attrs = GetAttributes(); - foreach (var key in attrs.Keys) - destination.SetAttribute(key, attrs[key]); - } + public override string ToString() + { + return $"Name: {GetName()}, root name: {MyRootGraph.GetName()}"; + } - return 0; - } + /// + /// Copy attributes from one cgraph object to another. + /// Throw argument exception if self and destination are not of the same type. + /// Also copies attributes that haven't been introduced in the destination object, + /// unless introduce_new_attrs is false. + /// Return code indicates success or failure. + /// + public int CopyAttributesTo(CGraphThing destination, bool introduce_new_attrs = true) + { + if (!((this is Node && destination is Node) + || (this is Edge && destination is Edge) + || (this is Graph && destination is Graph))) + throw new ArgumentException("Argument must be of the same type as self."); + + // Copying the attributes doesn't work if they have not been introduced in the graph + // Moreover, the copying may just stop at some point if copying of a single attribute fails + if (introduce_new_attrs && MyRootGraph._ptr != destination.MyRootGraph._ptr) + CloneAttributeDeclarations(MyRootGraph._ptr, destination.MyRootGraph._ptr); + + // agcopyattr returns non-zero number on failure. + + // Problems have been observed while copying between edges (of the same graph) + // Hypothesis: are the pointers different in this case? + // Hypothesis 2: does it have something to do with the comment "Do not copy key attribute for edges, + // as this must be distinct." in the graphviz source code? + // Returncode 1 has been observed while copying rootgraphs, while the number of attributes was 0 + Debug.Assert(_ptr != destination._ptr); + int success = Agcopyattr(_ptr, destination._ptr); + // We implement a workaround for copying between edges + if (success != 0) + { + // Fail for unknown failing cases + if (GetType() != typeof(Edge) && GetAttributes().Count != 0) + Debug.Fail("Copying attributes failed"); + + // We work around this doing the following: + var attrs = GetAttributes(); + foreach (var key in attrs.Keys) + destination.SetAttribute(key, attrs[key]); + } + + return 0; + } - /// - /// Some characters and character sequences have a special meaning. - /// If you intend to display a literal string, use this function to properly escape the string. - /// See also - /// https://www.graphviz.org/doc/info/shapes.html#record - /// https://www.graphviz.org/doc/info/attrs.html#k:escString - /// - public static string EscapeLabel(string label) + /// + /// Some characters and character sequences have a special meaning. + /// If you intend to display a literal string, use this function to properly escape the string. + /// See also + /// https://www.graphviz.org/doc/info/shapes.html#record + /// https://www.graphviz.org/doc/info/attrs.html#k:escString + /// + public static string EscapeLabel(string label) + { + // From the graphviz docs: + // Braces, vertical bars and angle brackets must be escaped with a backslash character if + // you wish them to appear as a literal character. Spaces are interpreted as separators + // between tokens, so they must be escaped if you want spaces in the text. + string result = label; + foreach (char c in new[] { '\\', '<', '>', '{', '}', ' ', '|' }) { - // From the graphviz docs: - // Braces, vertical bars and angle brackets must be escaped with a backslash character if - // you wish them to appear as a literal character. Spaces are interpreted as separators - // between tokens, so they must be escaped if you want spaces in the text. - string result = label; - foreach (char c in new[] { '\\', '<', '>', '{', '}', ' ', '|' }) - { - result = result.Replace(c.ToString(), "\\" + c); - } - return result; + result = result.Replace(c.ToString(), "\\" + c); } + return result; + } - #region layout functions - - public Color GetColor() - { - string colorstring = SafeGetAttribute("color", "Black"); - return Color.FromName(colorstring); - } + #region layout functions - public bool HasPosition() - { - return HasAttribute("pos"); - } + public Color GetColor() + { + string colorstring = SafeGetAttribute("color", "Black"); + return Color.FromName(colorstring); + } - public void MakeInvisible() - { - SafeSetAttribute("style", "invis", ""); - } + public bool HasPosition() + { + return HasAttribute("pos"); + } - public bool IsInvisible() - { - return SafeGetAttribute("style", "") == "invis"; - } + public void MakeInvisible() + { + SafeSetAttribute("style", "invis", ""); + } - protected static List GetXDotValue(CGraphThing obj, string attrName) - { - var xdotString = obj.SafeGetAttribute(attrName, null); - if (xdotString is null) - return new List(); + public bool IsInvisible() + { + return SafeGetAttribute("style", "") == "invis"; + } - return XDotParser.ParseXDot(xdotString); - } + protected static List GetXDotValue(CGraphThing obj, string attrName) + { + var xdotString = obj.SafeGetAttribute(attrName, null); + if (xdotString is null) + return new List(); - #endregion + return XDotParser.ParseXDot(xdotString); } + + #endregion } diff --git a/Rubjerg.Graphviz/Edge.cs b/Rubjerg.Graphviz/Edge.cs index dbb31d5..01a2bd9 100644 --- a/Rubjerg.Graphviz/Edge.cs +++ b/Rubjerg.Graphviz/Edge.cs @@ -5,177 +5,176 @@ using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public class Edge : CGraphThing { - public class Edge : CGraphThing - { - /// - /// rootgraph must not be null - /// - internal Edge(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } + /// + /// rootgraph must not be null + /// + internal Edge(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } - internal static Edge Get(Graph graph, Node tail, Node head, string name) - { - name = NameString(name); - IntPtr ptr = Agedge(graph._ptr, tail._ptr, head._ptr, name, 0); - if (ptr == IntPtr.Zero) - return null; - return new Edge(ptr, graph.MyRootGraph); - } + internal static Edge Get(Graph graph, Node tail, Node head, string name) + { + name = NameString(name); + IntPtr ptr = Agedge(graph._ptr, tail._ptr, head._ptr, name, 0); + if (ptr == IntPtr.Zero) + return null; + return new Edge(ptr, graph.MyRootGraph); + } - internal static Edge GetOrCreate(Graph graph, Node tail, Node head, string name) - { - name = NameString(name); - IntPtr ptr = Agedge(graph._ptr, tail._ptr, head._ptr, name, 1); - return new Edge(ptr, graph.MyRootGraph); - } + internal static Edge GetOrCreate(Graph graph, Node tail, Node head, string name) + { + name = NameString(name); + IntPtr ptr = Agedge(graph._ptr, tail._ptr, head._ptr, name, 1); + return new Edge(ptr, graph.MyRootGraph); + } - /// - /// Introduces an attribute for edges in the given graph by given a default. - /// A given default can be overwritten by calling this method again. - /// - public static void IntroduceAttribute(RootGraph root, string name, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - Agattr(root._ptr, 2, name, deflt); - } + /// + /// Introduces an attribute for edges in the given graph by given a default. + /// A given default can be overwritten by calling this method again. + /// + public static void IntroduceAttribute(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + Agattr(root._ptr, 2, name, deflt); + } - public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - AgattrHtml(root._ptr, 2, name, deflt); - } + public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + AgattrHtml(root._ptr, 2, name, deflt); + } - protected internal IntPtr HeadPtr() - { - return Aghead(_ptr); - } + protected internal IntPtr HeadPtr() + { + return Aghead(_ptr); + } - protected internal IntPtr TailPtr() - { - return Agtail(_ptr); - } + protected internal IntPtr TailPtr() + { + return Agtail(_ptr); + } - public Node Head() - { - return new Node(HeadPtr(), MyRootGraph); - } + public Node Head() + { + return new Node(HeadPtr(), MyRootGraph); + } - public Node Tail() - { - return new Node(TailPtr(), MyRootGraph); - } + public Node Tail() + { + return new Node(TailPtr(), MyRootGraph); + } - public Node OppositeEndpoint(Node node) - { - var tail = Tail(); - var head = Head(); - Debug.Assert(node == tail || node == head); - return node == tail ? head : tail; - } + public Node OppositeEndpoint(Node node) + { + var tail = Tail(); + var head = Head(); + Debug.Assert(node == tail || node == head); + return node == tail ? head : tail; + } - public bool IsAdjacentTo(Node node) - { - return node.Equals(Head()) || node.Equals(Tail()); - } + public bool IsAdjacentTo(Node node) + { + return node.Equals(Head()) || node.Equals(Tail()); + } - public bool IsBetween(Node node1, Node node2) - { - return IsAdjacentTo(node1) && IsAdjacentTo(node2); - } + public bool IsBetween(Node node1, Node node2) + { + return IsAdjacentTo(node1) && IsAdjacentTo(node2); + } - /// - /// An edge can define a cluster as logical tail. - /// This is used to fake edges to and from clusters by clipping the edge on the borders of the logical tail. - /// - /// - public void SetLogicalTail(SubGraph ltail) - { - if (!ltail.IsCluster()) - throw new InvalidOperationException("ltail must be a cluster"); - if (!MyRootGraph.IsCompound()) - throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); - string ltailname = ltail.GetName(); - SafeSetAttribute("ltail", ltailname, ""); - } + /// + /// An edge can define a cluster as logical tail. + /// This is used to fake edges to and from clusters by clipping the edge on the borders of the logical tail. + /// + /// + public void SetLogicalTail(SubGraph ltail) + { + if (!ltail.IsCluster()) + throw new InvalidOperationException("ltail must be a cluster"); + if (!MyRootGraph.IsCompound()) + throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); + string ltailname = ltail.GetName(); + SafeSetAttribute("ltail", ltailname, ""); + } - /// - /// An edge can define a cluster as logical head. - /// This is used to fake edges to and from clusters by clipping the edge on the borders of the logical head. - /// - public void SetLogicalHead(SubGraph lhead) - { - if (!lhead.IsCluster()) - throw new InvalidOperationException("ltail must be a cluster"); - if (!MyRootGraph.IsCompound()) - throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); - string lheadname = lhead.GetName(); - SafeSetAttribute("lhead", lheadname, ""); - } + /// + /// An edge can define a cluster as logical head. + /// This is used to fake edges to and from clusters by clipping the edge on the borders of the logical head. + /// + public void SetLogicalHead(SubGraph lhead) + { + if (!lhead.IsCluster()) + throw new InvalidOperationException("ltail must be a cluster"); + if (!MyRootGraph.IsCompound()) + throw new InvalidOperationException("rootgraph must be compound for lheads/ltails to be used"); + string lheadname = lhead.GetName(); + SafeSetAttribute("lhead", lheadname, ""); + } - /// - /// Port names cannot contain certain characters, and other characters must be escaped. - /// This function converts a string to an ID that is valid as a port name. - /// It makes sure there are no collisions. - /// - public static string ConvertUidToPortName(string id) + /// + /// Port names cannot contain certain characters, and other characters must be escaped. + /// This function converts a string to an ID that is valid as a port name. + /// It makes sure there are no collisions. + /// + public static string ConvertUidToPortName(string id) + { + string result = id; + foreach (char c in new[] { '<', '>', '{', '}', '|', ':' }) { - string result = id; - foreach (char c in new[] { '<', '>', '{', '}', '|', ':' }) - { - result = result.Replace("+", "[+]"); - result = result.Replace(c, '+'); - } - return result; + result = result.Replace("+", "[+]"); + result = result.Replace(c, '+'); } + return result; + } - // Because there are two valid pointers to each edge, we have to override the default equals behaviour - // which simply compares the wrapped pointers. - public override bool Equals(GraphvizThing obj) - { - if (obj is Edge) - return Ageqedge(_ptr, obj._ptr); - return false; - } + // Because there are two valid pointers to each edge, we have to override the default equals behaviour + // which simply compares the wrapped pointers. + public override bool Equals(GraphvizThing obj) + { + if (obj is Edge) + return Ageqedge(_ptr, obj._ptr); + return false; + } - public override int GetHashCode() - { - // Return the ptr to the in-edge, which is unique and consistent for each edge. - // The following line can result in an OverflowException: - //return (int) agmkin(ptr); - return (int)(long)Agmkin(_ptr); - } + public override int GetHashCode() + { + // Return the ptr to the in-edge, which is unique and consistent for each edge. + // The following line can result in an OverflowException: + //return (int) agmkin(ptr); + return (int)(long)Agmkin(_ptr); + } - #region layout attributes + #region layout attributes - /// - /// This method only returns the first spline that is defined. - /// Returns null if no splines exist. - /// - public PointF[] GetFirstSpline() - { - return GetSplines().FirstOrDefault(); - } + /// + /// This method only returns the first spline that is defined. + /// Returns null if no splines exist. + /// + public PointF[] GetFirstSpline() + { + return GetSplines().FirstOrDefault(); + } - /// - /// The splines contain 3n+1 points, just like expected by .net drawing methods. - /// Sometimes there are multiple splines per edge. However, this is not always correct: - /// https://github.com/ellson/graphviz/issues/1277 - /// Edge arrows are ignored. - /// - public IEnumerable GetSplines() - { - return GetDrawing().OfType() - .Select(x => x.Value.Points.Select(p => new PointF((float)p.X, (float)p.Y)).ToArray()); - } + /// + /// The splines contain 3n+1 points, just like expected by .net drawing methods. + /// Sometimes there are multiple splines per edge. However, this is not always correct: + /// https://github.com/ellson/graphviz/issues/1277 + /// Edge arrows are ignored. + /// + public IEnumerable GetSplines() + { + return GetDrawing().OfType() + .Select(x => x.Value.Points.Select(p => new PointF((float)p.X, (float)p.Y)).ToArray()); + } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); - public IReadOnlyList GetHeadArrowDrawing() => GetXDotValue(this, "_hdraw_"); - public IReadOnlyList GetTailArrowDrawing() => GetXDotValue(this, "_tdraw_"); - public IReadOnlyList GetHeadLabelDrawing() => GetXDotValue(this, "_hldraw_"); - public IReadOnlyList GetTailLabelDrawing() => GetXDotValue(this, "_tldraw_"); + public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); + public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + public IReadOnlyList GetHeadArrowDrawing() => GetXDotValue(this, "_hdraw_"); + public IReadOnlyList GetTailArrowDrawing() => GetXDotValue(this, "_tdraw_"); + public IReadOnlyList GetHeadLabelDrawing() => GetXDotValue(this, "_hldraw_"); + public IReadOnlyList GetTailLabelDrawing() => GetXDotValue(this, "_tldraw_"); - #endregion - } + #endregion } diff --git a/Rubjerg.Graphviz/ForeignFunctionInterface.cs b/Rubjerg.Graphviz/ForeignFunctionInterface.cs index 23b77e5..2653eb0 100644 --- a/Rubjerg.Graphviz/ForeignFunctionInterface.cs +++ b/Rubjerg.Graphviz/ForeignFunctionInterface.cs @@ -1,707 +1,706 @@ using System; using System.Runtime.InteropServices; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +/// +/// Graphviz is thread unsafe, so we wrap all function calls inside a lock to make sure we don't run into +/// issues caused by multiple threads accessing the graphviz datastructures (like the GC executing a destructor). +/// +internal static class ForeignFunctionInterface { - /// - /// Graphviz is thread unsafe, so we wrap all function calls inside a lock to make sure we don't run into - /// issues caused by multiple threads accessing the graphviz datastructures (like the GC executing a destructor). - /// - internal static class ForeignFunctionInterface - { - private static readonly object _mutex = new object(); + private static readonly object _mutex = new object(); - public static IntPtr GvContext() + public static IntPtr GvContext() + { + lock (_mutex) { - lock (_mutex) - { - return gvContext(); - } + return gvContext(); } - public static int GvFreeContext(IntPtr gvc) + } + public static int GvFreeContext(IntPtr gvc) + { + lock (_mutex) { - lock (_mutex) - { - return gvFreeContext(gvc); - } + return gvFreeContext(gvc); } - public static int GvLayout(IntPtr gvc, IntPtr graph, string engine) + } + public static int GvLayout(IntPtr gvc, IntPtr graph, string engine) + { + lock (_mutex) { - lock (_mutex) - { - return gvLayout(gvc, graph, engine); - } + return gvLayout(gvc, graph, engine); } - public static int GvFreeLayout(IntPtr gvc, IntPtr graph) + } + public static int GvFreeLayout(IntPtr gvc, IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - return gvFreeLayout(gvc, graph); - } + return gvFreeLayout(gvc, graph); } - public static int GvRender(IntPtr gvc, IntPtr graph, string format, IntPtr @out) + } + public static int GvRender(IntPtr gvc, IntPtr graph, string format, IntPtr @out) + { + lock (_mutex) { - lock (_mutex) - { - return gvRender(gvc, graph, format, @out); - } + return gvRender(gvc, graph, format, @out); } - public static int GvRenderFilename(IntPtr gvc, IntPtr graph, string format, string filename) + } + public static int GvRenderFilename(IntPtr gvc, IntPtr graph, string format, string filename) + { + lock (_mutex) { - lock (_mutex) - { - return gvRenderFilename(gvc, graph, format, filename); - } + return gvRenderFilename(gvc, graph, format, filename); } - public static IntPtr Agnode(IntPtr graph, string name, int create) + } + public static IntPtr Agnode(IntPtr graph, string name, int create) + { + lock (_mutex) { - lock (_mutex) - { - return agnode(graph, name, create); - } + return agnode(graph, name, create); } - public static int Agdegree(IntPtr graph, IntPtr node, int inset, int outset) + } + public static int Agdegree(IntPtr graph, IntPtr node, int inset, int outset) + { + lock (_mutex) { - lock (_mutex) - { - return agdegree(graph, node, inset, outset); - } + return agdegree(graph, node, inset, outset); } - public static IntPtr Agfstout(IntPtr graph, IntPtr node) + } + public static IntPtr Agfstout(IntPtr graph, IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return agfstout(graph, node); - } + return agfstout(graph, node); } - public static IntPtr Agnxtout(IntPtr graph, IntPtr edge) + } + public static IntPtr Agnxtout(IntPtr graph, IntPtr edge) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtout(graph, edge); - } + return agnxtout(graph, edge); } - public static IntPtr Agfstin(IntPtr graph, IntPtr node) + } + public static IntPtr Agfstin(IntPtr graph, IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return agfstin(graph, node); - } + return agfstin(graph, node); } - public static IntPtr Agnxtin(IntPtr graph, IntPtr edge) + } + public static IntPtr Agnxtin(IntPtr graph, IntPtr edge) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtin(graph, edge); - } + return agnxtin(graph, edge); } - public static IntPtr Agfstedge(IntPtr graph, IntPtr node) + } + public static IntPtr Agfstedge(IntPtr graph, IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return agfstedge(graph, node); - } + return agfstedge(graph, node); } - public static IntPtr Agnxtedge(IntPtr graph, IntPtr edge, IntPtr node) + } + public static IntPtr Agnxtedge(IntPtr graph, IntPtr edge, IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtedge(graph, edge, node); - } + return agnxtedge(graph, edge, node); } - public static void Agattr(IntPtr graph, int type, string name, string deflt) + } + public static void Agattr(IntPtr graph, int type, string name, string deflt) + { + lock (_mutex) { - lock (_mutex) - { - agattr(graph, type, name, deflt); - } + agattr(graph, type, name, deflt); } - public static void AgattrHtml(IntPtr graph, int type, string name, string deflt) + } + public static void AgattrHtml(IntPtr graph, int type, string name, string deflt) + { + lock (_mutex) { - lock (_mutex) - { - var ptr = agstrdup_html(agroot(graph), deflt); - agattr(graph, type, name, ptr); - } + var ptr = agstrdup_html(agroot(graph), deflt); + agattr(graph, type, name, ptr); } + } - public static void Agset(IntPtr obj, string name, string value) + public static void Agset(IntPtr obj, string name, string value) + { + lock (_mutex) { - lock (_mutex) - { - agset(obj, name, value); - } + agset(obj, name, value); } + } - public static void AgsetHtml(IntPtr obj, string name, string value) + public static void AgsetHtml(IntPtr obj, string name, string value) + { + lock (_mutex) { - lock (_mutex) - { - var ptr = agstrdup_html(agroot(obj), value); - agset(obj, name, ptr); - } + var ptr = agstrdup_html(agroot(obj), value); + agset(obj, name, ptr); } + } - public static void Agsafeset(IntPtr obj, string name, string val, string deflt) + public static void Agsafeset(IntPtr obj, string name, string val, string deflt) + { + lock (_mutex) { - lock (_mutex) - { - agsafeset(obj, name, val, deflt); - } + agsafeset(obj, name, val, deflt); } - public static void AgsafesetHtml(IntPtr obj, string name, string val, string deflt) + } + public static void AgsafesetHtml(IntPtr obj, string name, string val, string deflt) + { + lock (_mutex) { - lock (_mutex) - { - var ptr = agstrdup_html(agroot(obj), deflt); - agsafeset(obj, name, val, ptr); - } + var ptr = agstrdup_html(agroot(obj), deflt); + agsafeset(obj, name, val, ptr); } - public static IntPtr Agroot(IntPtr obj) + } + public static IntPtr Agroot(IntPtr obj) + { + lock (_mutex) { - lock (_mutex) - { - return agroot(obj); - } + return agroot(obj); } - public static IntPtr Agnxtattr(IntPtr obj, int kind, IntPtr attribute) + } + public static IntPtr Agnxtattr(IntPtr obj, int kind, IntPtr attribute) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtattr(obj, kind, attribute); - } + return agnxtattr(obj, kind, attribute); } - public static int Agcopyattr(IntPtr from, IntPtr to) + } + public static int Agcopyattr(IntPtr from, IntPtr to) + { + lock (_mutex) { - lock (_mutex) - { - return agcopyattr(from, to); - } + return agcopyattr(from, to); } - public static bool Ageqedge(IntPtr edge1, IntPtr edge2) + } + public static bool Ageqedge(IntPtr edge1, IntPtr edge2) + { + lock (_mutex) { - lock (_mutex) - { - return rj_ageqedge(edge1, edge2); - } + return rj_ageqedge(edge1, edge2); } - public static IntPtr Agtail(IntPtr node) + } + public static IntPtr Agtail(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return rj_agtail(node); - } + return rj_agtail(node); } - public static IntPtr Aghead(IntPtr node) + } + public static IntPtr Aghead(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return rj_aghead(node); - } + return rj_aghead(node); } - public static IntPtr Agedge(IntPtr graph, IntPtr tail, IntPtr head, string name, int create) + } + public static IntPtr Agedge(IntPtr graph, IntPtr tail, IntPtr head, string name, int create) + { + lock (_mutex) { - lock (_mutex) - { - return agedge(graph, tail, head, name, create); - } + return agedge(graph, tail, head, name, create); } - public static IntPtr Agmkin(IntPtr edge) + } + public static IntPtr Agmkin(IntPtr edge) + { + lock (_mutex) { - lock (_mutex) - { - return rj_agmkin(edge); - } + return rj_agmkin(edge); } - public static IntPtr Agmkout(IntPtr edge) + } + public static IntPtr Agmkout(IntPtr edge) + { + lock (_mutex) { - lock (_mutex) - { - return rj_agmkout(edge); - } + return rj_agmkout(edge); } - public static IntPtr Agparent(IntPtr obj) + } + public static IntPtr Agparent(IntPtr obj) + { + lock (_mutex) { - lock (_mutex) - { - return agparent(obj); - } + return agparent(obj); } - public static int Agclose(IntPtr graph) + } + public static int Agclose(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - return agclose(graph); - } + return agclose(graph); } - public static int Agdelete(IntPtr graph, IntPtr item) + } + public static int Agdelete(IntPtr graph, IntPtr item) + { + lock (_mutex) { - lock (_mutex) - { - return agdelete(graph, item); - } + return agdelete(graph, item); } - public static IntPtr Agfstnode(IntPtr graph) + } + public static IntPtr Agfstnode(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - return agfstnode(graph); - } + return agfstnode(graph); } - public static IntPtr Agnxtnode(IntPtr graph, IntPtr node) + } + public static IntPtr Agnxtnode(IntPtr graph, IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtnode(graph, node); - } + return agnxtnode(graph, node); } - public static int Agcontains(IntPtr graph, IntPtr obj) + } + public static int Agcontains(IntPtr graph, IntPtr obj) + { + lock (_mutex) { - lock (_mutex) - { - return agcontains(graph, obj); - } + return agcontains(graph, obj); } - public static IntPtr Agsubg(IntPtr graph, string name, int create) + } + public static IntPtr Agsubg(IntPtr graph, string name, int create) + { + lock (_mutex) { - lock (_mutex) - { - return agsubg(graph, name, create); - } + return agsubg(graph, name, create); } - public static IntPtr Agfstsubg(IntPtr graph) + } + public static IntPtr Agfstsubg(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - return agfstsubg(graph); - } + return agfstsubg(graph); } - public static IntPtr Agnxtsubg(IntPtr graph) + } + public static IntPtr Agnxtsubg(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - return agnxtsubg(graph); - } + return agnxtsubg(graph); } - public static int Agisstrict(IntPtr ptr) + } + public static int Agisstrict(IntPtr ptr) + { + lock (_mutex) { - lock (_mutex) - { - return agisstrict(ptr); - } + return agisstrict(ptr); } - public static int Agisdirected(IntPtr ptr) + } + public static int Agisdirected(IntPtr ptr) + { + lock (_mutex) { - lock (_mutex) - { - return agisdirected(ptr); - } + return agisdirected(ptr); } - public static int Agisundirected(IntPtr ptr) + } + public static int Agisundirected(IntPtr ptr) + { + lock (_mutex) { - lock (_mutex) - { - return agisundirected(ptr); - } + return agisundirected(ptr); } - public static IntPtr Agsubedge(IntPtr graph, IntPtr edge, int create) + } + public static IntPtr Agsubedge(IntPtr graph, IntPtr edge, int create) + { + lock (_mutex) { - lock (_mutex) - { - return agsubedge(graph, edge, create); - } + return agsubedge(graph, edge, create); } - public static IntPtr Agsubnode(IntPtr graph, IntPtr node, int create) + } + public static IntPtr Agsubnode(IntPtr graph, IntPtr node, int create) + { + lock (_mutex) { - lock (_mutex) - { - return agsubnode(graph, node, create); - } + return agsubnode(graph, node, create); } - public static IntPtr EdgeLabel(IntPtr node) + } + public static IntPtr EdgeLabel(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return edge_label(node); - } + return edge_label(node); } - public static string Rjagmemwrite(IntPtr graph) + } + public static string Rjagmemwrite(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - var ptr = rj_agmemwrite(graph); - var result = Marshal.PtrToStringAnsi(ptr); - free_str(ptr); - return result; - } + var ptr = rj_agmemwrite(graph); + var result = Marshal.PtrToStringAnsi(ptr); + free_str(ptr); + return result; } - public static IntPtr GraphLabel(IntPtr node) + } + public static IntPtr GraphLabel(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return graph_label(node); - } + return graph_label(node); } - public static string Agget(IntPtr obj, string name) + } + public static string Agget(IntPtr obj, string name) + { + lock (_mutex) { - lock (_mutex) - { - return Marshal.PtrToStringAnsi(agget(obj, name)); - } + return Marshal.PtrToStringAnsi(agget(obj, name)); } - public static string Rjagnameof(IntPtr obj) + } + public static string Rjagnameof(IntPtr obj) + { + lock (_mutex) { - lock (_mutex) - { - return Marshal.PtrToStringAnsi(agnameof(obj)); - } + return Marshal.PtrToStringAnsi(agnameof(obj)); } - public static void CloneAttributeDeclarations(IntPtr graphfrom, IntPtr graphto) + } + public static void CloneAttributeDeclarations(IntPtr graphfrom, IntPtr graphto) + { + lock (_mutex) { - lock (_mutex) - { - clone_attribute_declarations(graphfrom, graphto); - } + clone_attribute_declarations(graphfrom, graphto); } - public static string ImsymKey(IntPtr sym) + } + public static string ImsymKey(IntPtr sym) + { + lock (_mutex) { - lock (_mutex) - { - return Marshal.PtrToStringAnsi(rj_sym_key(sym)); - } + return Marshal.PtrToStringAnsi(rj_sym_key(sym)); } - public static double LabelX(IntPtr label) + } + public static double LabelX(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return label_x(label); - } + return label_x(label); } - public static double LabelY(IntPtr label) + } + public static double LabelY(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return label_y(label); - } + return label_y(label); } - public static double LabelWidth(IntPtr label) + } + public static double LabelWidth(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return label_width(label); - } + return label_width(label); } - public static double LabelHeight(IntPtr label) + } + public static double LabelHeight(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return label_height(label); - } + return label_height(label); } - public static string LabelText(IntPtr label) + } + public static string LabelText(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return Marshal.PtrToStringAnsi(label_text(label)); - } + return Marshal.PtrToStringAnsi(label_text(label)); } - public static double LabelFontsize(IntPtr label) + } + public static double LabelFontsize(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return label_fontsize(label); - } + return label_fontsize(label); } - public static string LabelFontname(IntPtr label) + } + public static string LabelFontname(IntPtr label) + { + lock (_mutex) { - lock (_mutex) - { - return Marshal.PtrToStringAnsi(label_fontname(label)); - } + return Marshal.PtrToStringAnsi(label_fontname(label)); } - public static double NodeX(IntPtr node) + } + public static double NodeX(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return node_x(node); - } + return node_x(node); } - public static double NodeY(IntPtr node) + } + public static double NodeY(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return node_y(node); - } + return node_y(node); } - public static double NodeWidth(IntPtr node) + } + public static double NodeWidth(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return node_width(node); - } + return node_width(node); } - public static double NodeHeight(IntPtr node) + } + public static double NodeHeight(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return node_height(node); - } + return node_height(node); } - public static IntPtr NodeLabel(IntPtr node) + } + public static IntPtr NodeLabel(IntPtr node) + { + lock (_mutex) { - lock (_mutex) - { - return node_label(node); - } + return node_label(node); } - public static void ConvertToUndirected(IntPtr graph) + } + public static void ConvertToUndirected(IntPtr graph) + { + lock (_mutex) { - lock (_mutex) - { - convert_to_undirected(graph); - } + convert_to_undirected(graph); } - public static IntPtr Rjagmemread(string input) + } + public static IntPtr Rjagmemread(string input) + { + lock (_mutex) { - lock (_mutex) - { - return rj_agmemread(input); - } + return rj_agmemread(input); } - public static IntPtr Rjagopen(string name, int graphtype) + } + public static IntPtr Rjagopen(string name, int graphtype) + { + lock (_mutex) { - lock (_mutex) - { - return rj_agopen(name, graphtype); - } + return rj_agopen(name, graphtype); } + } - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void free_str(IntPtr ptr); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void free_str(IntPtr ptr); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr gvContext(); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int gvFreeContext(IntPtr gvc); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int gvLayout(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string engine); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int gvFreeLayout(IntPtr gvc, IntPtr graph); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int gvRender(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string format, IntPtr @out); - [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int gvRenderFilename(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string format, [MarshalAs(UnmanagedType.LPStr)] string filename); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr gvContext(); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int gvFreeContext(IntPtr gvc); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int gvLayout(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string engine); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int gvFreeLayout(IntPtr gvc, IntPtr graph); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int gvRender(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string format, IntPtr @out); + [DllImport("gvc.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int gvRenderFilename(IntPtr gvc, IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string format, [MarshalAs(UnmanagedType.LPStr)] string filename); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnode(IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string name, int create); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agdegree(IntPtr graph, IntPtr node, int inset, int outset); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agfstout(IntPtr graph, IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtout(IntPtr graph, IntPtr edge); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agfstin(IntPtr graph, IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtin(IntPtr graph, IntPtr edge); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agfstedge(IntPtr graph, IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtedge(IntPtr graph, IntPtr edge, IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnode(IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string name, int create); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agdegree(IntPtr graph, IntPtr node, int inset, int outset); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agfstout(IntPtr graph, IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtout(IntPtr graph, IntPtr edge); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agfstin(IntPtr graph, IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtin(IntPtr graph, IntPtr edge); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agfstedge(IntPtr graph, IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtedge(IntPtr graph, IntPtr edge, IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agattr(IntPtr graph, int type, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string deflt); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agattr(IntPtr graph, int type, [MarshalAs(UnmanagedType.LPStr)] string name, IntPtr deflt); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, IntPtr value); - [DllImport("cgraph.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agstrdup_html(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string html); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agsafeset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string val, [MarshalAs(UnmanagedType.LPStr)] string deflt); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void agsafeset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string val, IntPtr deflt); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agroot(IntPtr obj); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtattr(IntPtr obj, int kind, IntPtr attribute); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agcopyattr(IntPtr from, IntPtr to); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agattr(IntPtr graph, int type, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string deflt); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agattr(IntPtr graph, int type, [MarshalAs(UnmanagedType.LPStr)] string name, IntPtr deflt); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string value); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, IntPtr value); + [DllImport("cgraph.dll", SetLastError = false, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agstrdup_html(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string html); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agsafeset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string val, [MarshalAs(UnmanagedType.LPStr)] string deflt); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void agsafeset(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name, [MarshalAs(UnmanagedType.LPStr)] string val, IntPtr deflt); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agroot(IntPtr obj); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtattr(IntPtr obj, int kind, IntPtr attribute); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agcopyattr(IntPtr from, IntPtr to); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.U1)] - private static extern bool rj_ageqedge(IntPtr edge1, IntPtr edge2); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agtail(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_aghead(IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agedge(IntPtr graph, IntPtr tail, IntPtr head, [MarshalAs(UnmanagedType.LPStr)] string name, int create); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agmkin(IntPtr edge); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agmkout(IntPtr edge); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool rj_ageqedge(IntPtr edge1, IntPtr edge2); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agtail(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_aghead(IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agedge(IntPtr graph, IntPtr tail, IntPtr head, [MarshalAs(UnmanagedType.LPStr)] string name, int create); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agmkin(IntPtr edge); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agmkout(IntPtr edge); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agparent(IntPtr obj); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agparent(IntPtr obj); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agclose(IntPtr graph); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agdelete(IntPtr graph, IntPtr item); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agfstnode(IntPtr graph); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtnode(IntPtr graph, IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agcontains(IntPtr graph, IntPtr obj); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agclose(IntPtr graph); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agdelete(IntPtr graph, IntPtr item); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agfstnode(IntPtr graph); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtnode(IntPtr graph, IntPtr node); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agcontains(IntPtr graph, IntPtr obj); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agsubg(IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string name, int create); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agfstsubg(IntPtr graph); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnxtsubg(IntPtr graph); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agisstrict(IntPtr ptr); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agisdirected(IntPtr ptr); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern int agisundirected(IntPtr ptr); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agsubg(IntPtr graph, [MarshalAs(UnmanagedType.LPStr)] string name, int create); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agfstsubg(IntPtr graph); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnxtsubg(IntPtr graph); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agisstrict(IntPtr ptr); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agisdirected(IntPtr ptr); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern int agisundirected(IntPtr ptr); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agsubedge(IntPtr graph, IntPtr edge, int create); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agsubnode(IntPtr graph, IntPtr node, int create); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agsubedge(IntPtr graph, IntPtr edge, int create); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agsubnode(IntPtr graph, IntPtr node, int create); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr edge_label(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agmemwrite(IntPtr graph); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr graph_label(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr edge_label(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agmemwrite(IntPtr graph); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr graph_label(IntPtr node); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agget(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name); - [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr agnameof(IntPtr obj); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void clone_attribute_declarations(IntPtr graphfrom, IntPtr graphto); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_sym_key(IntPtr sym); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agget(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string name); + [DllImport("cgraph.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr agnameof(IntPtr obj); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void clone_attribute_declarations(IntPtr graphfrom, IntPtr graphto); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_sym_key(IntPtr sym); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double label_x(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double label_y(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double label_width(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double label_height(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr label_text(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double label_fontsize(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr label_fontname(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double label_x(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double label_y(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double label_width(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double label_height(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr label_text(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double label_fontsize(IntPtr label); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr label_fontname(IntPtr label); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double node_x(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double node_y(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double node_width(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern double node_height(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr node_label(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double node_x(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double node_y(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double node_width(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern double node_height(IntPtr node); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr node_label(IntPtr node); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern void convert_to_undirected(IntPtr graph); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agmemread([MarshalAs(UnmanagedType.LPStr)] string input); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr rj_agopen([MarshalAs(UnmanagedType.LPStr)] string name, int graphtype); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern void convert_to_undirected(IntPtr graph); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agmemread([MarshalAs(UnmanagedType.LPStr)] string input); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr rj_agopen([MarshalAs(UnmanagedType.LPStr)] string name, int graphtype); - /// - /// A GraphvizContext is used to store various layout - /// information that is independent of a particular graph and - /// its attributes. It holds the data associated with plugins, - /// parsed - command lines, script engines, and anything else - /// with a scope potentially larger than one graph, up to the - /// scope of the application. In addition, it maintains lists of - /// the available layout algorithms and renderers; it also - /// records the most recent layout algorithm applied to a graph. - /// It can be used to specify multiple renderings of a given - /// graph layout into different associated files.It is also used - /// to store various global information used during rendering. - /// There should be just one GVC created for the entire - /// duration of an application. A single GVC value can be used - /// with multiple graphs, though with only one graph at a - /// time. In addition, if gvLayout() was invoked for a graph and - /// GVC, then gvFreeLayout() should be called before using - /// gvLayout() again, even on the same graph. - /// - public static IntPtr GVC { get; private set; } - static ForeignFunctionInterface() - { - // We initialize the gvc here before interacting with graphviz - // https://gitlab.com/graphviz/graphviz/-/issues/2434 - GVC = GvContext(); - } + /// + /// A GraphvizContext is used to store various layout + /// information that is independent of a particular graph and + /// its attributes. It holds the data associated with plugins, + /// parsed - command lines, script engines, and anything else + /// with a scope potentially larger than one graph, up to the + /// scope of the application. In addition, it maintains lists of + /// the available layout algorithms and renderers; it also + /// records the most recent layout algorithm applied to a graph. + /// It can be used to specify multiple renderings of a given + /// graph layout into different associated files.It is also used + /// to store various global information used during rendering. + /// There should be just one GVC created for the entire + /// duration of an application. A single GVC value can be used + /// with multiple graphs, though with only one graph at a + /// time. In addition, if gvLayout() was invoked for a graph and + /// GVC, then gvFreeLayout() should be called before using + /// gvLayout() again, even on the same graph. + /// + public static IntPtr GVC { get; private set; } + static ForeignFunctionInterface() + { + // We initialize the gvc here before interacting with graphviz + // https://gitlab.com/graphviz/graphviz/-/issues/2434 + GVC = GvContext(); + } - #region debugging and testing + #region debugging and testing - // .NET uses UnmanagedType.Bool by default for P/Invoke, but our C++ code uses UnmanagedType.U1 - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool echobool([MarshalAs(UnmanagedType.U1)] bool arg); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool return_true(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool return_false(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int echoint(int arg); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int return1(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int return_1(); + // .NET uses UnmanagedType.Bool by default for P/Invoke, but our C++ code uses UnmanagedType.U1 + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool echobool([MarshalAs(UnmanagedType.U1)] bool arg); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool return_true(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool return_false(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int echoint(int arg); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int return1(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int return_1(); - public enum TestEnum - { - Val1, Val2, Val3, Val4, Val5 - } - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern TestEnum return_enum1(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern TestEnum return_enum2(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern TestEnum return_enum5(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern TestEnum echo_enum(TestEnum e); + public enum TestEnum + { + Val1, Val2, Val3, Val4, Val5 + } + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern TestEnum return_enum1(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern TestEnum return_enum2(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern TestEnum return_enum5(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern TestEnum echo_enum(TestEnum e); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr echo_string([MarshalAs(UnmanagedType.LPStr)] string str); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr return_empty_string(); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr return_hello(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr echo_string([MarshalAs(UnmanagedType.LPStr)] string str); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr return_empty_string(); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr return_hello(); - public static string EchoString(string str) - { - // echo_string gives us ownership over the string, which means that we have to free it. - var ptr = echo_string(str); - var result = Marshal.PtrToStringAnsi(ptr); - free_str(ptr); - return result; - } - public static string ReturnEmptyString() => Marshal.PtrToStringAnsi(return_empty_string()); - public static string ReturnHello() => Marshal.PtrToStringAnsi(return_hello()); - #endregion + public static string EchoString(string str) + { + // echo_string gives us ownership over the string, which means that we have to free it. + var ptr = echo_string(str); + var result = Marshal.PtrToStringAnsi(ptr); + free_str(ptr); + return result; } + public static string ReturnEmptyString() => Marshal.PtrToStringAnsi(return_empty_string()); + public static string ReturnHello() => Marshal.PtrToStringAnsi(return_hello()); + #endregion } diff --git a/Rubjerg.Graphviz/Graph.cs b/Rubjerg.Graphviz/Graph.cs index 3ad436b..d5f1635 100644 --- a/Rubjerg.Graphviz/Graph.cs +++ b/Rubjerg.Graphviz/Graph.cs @@ -7,658 +7,657 @@ using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +/// +/// Wraps a cgraph graph object - either a subgraph or a rootgraph. +/// +public class Graph : CGraphThing { /// - /// Wraps a cgraph graph object - either a subgraph or a rootgraph. + /// rootgraph may be null /// - public class Graph : CGraphThing - { - /// - /// rootgraph may be null - /// - protected Graph(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } - public bool IsStrict() { return Agisstrict(_ptr) != 0; } - public bool IsDirected() { return Agisdirected(_ptr) != 0; } - public bool IsUndirected() { return Agisundirected(_ptr) != 0; } - public GraphType GetGraphType() - { - if (IsDirected() && !IsStrict()) - return GraphType.Directed; - if (IsDirected() && IsStrict()) - return GraphType.StrictDirected; - if (IsUndirected() && !IsStrict()) - return GraphType.Undirected; - return GraphType.StrictUndirected; - } + protected Graph(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } + public bool IsStrict() { return Agisstrict(_ptr) != 0; } + public bool IsDirected() { return Agisdirected(_ptr) != 0; } + public bool IsUndirected() { return Agisundirected(_ptr) != 0; } + public GraphType GetGraphType() + { + if (IsDirected() && !IsStrict()) + return GraphType.Directed; + if (IsDirected() && IsStrict()) + return GraphType.StrictDirected; + if (IsUndirected() && !IsStrict()) + return GraphType.Undirected; + return GraphType.StrictUndirected; + } - /// - /// Introduces an attribute for subgraphs of the given graph by given a default. - /// A given default can be overwritten by calling this method again. - /// - public static void IntroduceAttribute(RootGraph root, string name, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - Agattr(root._ptr, 0, name, deflt); - } + /// + /// Introduces an attribute for subgraphs of the given graph by given a default. + /// A given default can be overwritten by calling this method again. + /// + public static void IntroduceAttribute(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + Agattr(root._ptr, 0, name, deflt); + } - public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - AgattrHtml(root._ptr, 0, name, deflt); - } + public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + AgattrHtml(root._ptr, 0, name, deflt); + } - public bool Contains(CGraphThing thing) - { - return Agcontains(_ptr, thing._ptr) != 0; - } + public bool Contains(CGraphThing thing) + { + return Agcontains(_ptr, thing._ptr) != 0; + } - /// - /// Delete the given thing from the graph. - /// Is this is a root graph, and thing is a node, its edges are delete too. - /// - public void Delete(CGraphThing thing) - { - int return_code = Agdelete(_ptr, thing._ptr); - Debug.Assert(return_code == 0); - } + /// + /// Delete the given thing from the graph. + /// Is this is a root graph, and thing is a node, its edges are delete too. + /// + public void Delete(CGraphThing thing) + { + int return_code = Agdelete(_ptr, thing._ptr); + Debug.Assert(return_code == 0); + } - public IEnumerable Nodes() + public IEnumerable Nodes() + { + var current = Agfstnode(_ptr); + while (current != IntPtr.Zero) { - var current = Agfstnode(_ptr); - while (current != IntPtr.Zero) - { - yield return new Node(current, MyRootGraph); - current = Agnxtnode(_ptr, current); - } + yield return new Node(current, MyRootGraph); + current = Agnxtnode(_ptr, current); } + } - public IEnumerable Edges() - { - return Nodes().SelectMany(n => n.EdgesOut()); - } + public IEnumerable Edges() + { + return Nodes().SelectMany(n => n.EdgesOut()); + } - public IEnumerable Children() + public IEnumerable Children() + { + var current = Agfstsubg(_ptr); + while (current != IntPtr.Zero) { - var current = Agfstsubg(_ptr); - while (current != IntPtr.Zero) - { - yield return new SubGraph(current, MyRootGraph); - current = Agnxtsubg(current); - } + yield return new SubGraph(current, MyRootGraph); + current = Agnxtsubg(current); } + } - /// - /// Return all subgraphs recursively in a depthfirst order. - /// Don't delete subgraphs while iterating. Instead, use the method SafeDeleteSubgraphs. - /// - public IEnumerable Descendants() + /// + /// Return all subgraphs recursively in a depthfirst order. + /// Don't delete subgraphs while iterating. Instead, use the method SafeDeleteSubgraphs. + /// + public IEnumerable Descendants() + { + foreach (var child in Children()) { - foreach (var child in Children()) - { - yield return child; - foreach (SubGraph descendant in child.Descendants()) - yield return descendant; - } + yield return child; + foreach (SubGraph descendant in child.Descendants()) + yield return descendant; } + } - public Graph Parent() - { - IntPtr p = Agparent(_ptr); - if (p == IntPtr.Zero) - return null; - return new Graph(p, MyRootGraph); - } + public Graph Parent() + { + IntPtr p = Agparent(_ptr); + if (p == IntPtr.Zero) + return null; + return new Graph(p, MyRootGraph); + } - public IEnumerable GetNodesByAttribute(string attr_name, string attr_value) - { - return Nodes().Where(n => n.GetAttribute(attr_name) == attr_value); - } + public IEnumerable GetNodesByAttribute(string attr_name, string attr_value) + { + return Nodes().Where(n => n.GetAttribute(attr_name) == attr_value); + } - public SubGraph GetDescendantByName(string name) - { - return Descendants().FirstOrDefault(d => d.GetName() == name); - } + public SubGraph GetDescendantByName(string name) + { + return Descendants().FirstOrDefault(d => d.GetName() == name); + } - public SubGraph GetOrAddSubgraph(string name) - { - return SubGraph.GetOrCreate(this, name); - } + public SubGraph GetOrAddSubgraph(string name) + { + return SubGraph.GetOrCreate(this, name); + } - public SubGraph GetSubgraph(string name) - { - return SubGraph.Get(this, name); - } + public SubGraph GetSubgraph(string name) + { + return SubGraph.Get(this, name); + } - public Node GetOrAddNode(string name) - { - return Node.GetOrCreate(this, name); - } + public Node GetOrAddNode(string name) + { + return Node.GetOrCreate(this, name); + } - public Node GetNode(string name) - { - return Node.Get(this, name); - } + public Node GetNode(string name) + { + return Node.Get(this, name); + } - /// - /// Passing null as edge name (the default) will result in a new unique edge without a name. - /// Passing the empty string has the same effect as passing null. - /// - public Edge GetOrAddEdge(Node tail, Node head, string name = null) - { - return Edge.GetOrCreate(this, tail, head, name); - } + /// + /// Passing null as edge name (the default) will result in a new unique edge without a name. + /// Passing the empty string has the same effect as passing null. + /// + public Edge GetOrAddEdge(Node tail, Node head, string name = null) + { + return Edge.GetOrCreate(this, tail, head, name); + } - /// - /// Passing null as edge name will return any edge between the given endpoints, regardless of their name. - /// Passing the empty string has the same effect as passing null. - /// - public Edge GetEdge(Node tail, Node head, string name = null) - { - return Edge.Get(this, tail, head, name); - } + /// + /// Passing null as edge name will return any edge between the given endpoints, regardless of their name. + /// Passing the empty string has the same effect as passing null. + /// + public Edge GetEdge(Node tail, Node head, string name = null) + { + return Edge.Get(this, tail, head, name); + } - ///Return an edge between two nodes, disregarding it's direction. - /// - /// Passing null as edge name will return any edge between the given endpoints, regardless of their name. - /// Passing the empty string has the same effect as passing null. - /// - public Edge GetEdgeBetween(Node node1, Node node2, string name = null) - { - return Edge.Get(this, node1, node2, name) ?? Edge.Get(this, node2, node1, name); - } + ///Return an edge between two nodes, disregarding it's direction. + /// + /// Passing null as edge name will return any edge between the given endpoints, regardless of their name. + /// Passing the empty string has the same effect as passing null. + /// + public Edge GetEdgeBetween(Node node1, Node node2, string name = null) + { + return Edge.Get(this, node1, node2, name) ?? Edge.Get(this, node2, node1, name); + } - public IEnumerable GetEdgesBetween(Node node1, Node node2) - { - return node1.Edges().Where(edge => edge.IsBetween(node1, node2)); - } + public IEnumerable GetEdgesBetween(Node node1, Node node2) + { + return node1.Edges().Where(edge => edge.IsBetween(node1, node2)); + } - public override string ToString() - { - return $"Graph {GetName()} with {Nodes().Count()} nodes."; - } + public override string ToString() + { + return $"Graph {GetName()} with {Nodes().Count()} nodes."; + } - /// - /// Attributes with the empty string as default are not correctly exported. - /// https://gitlab.com/graphviz/graphviz/-/issues/1887 - /// - public string ToDotString() - { - return Rjagmemwrite(_ptr); - } + /// + /// Attributes with the empty string as default are not correctly exported. + /// https://gitlab.com/graphviz/graphviz/-/issues/1887 + /// + public string ToDotString() + { + return Rjagmemwrite(_ptr); + } - /// - /// Attributes with the empty string as default are not correctly exported. - /// https://gitlab.com/graphviz/graphviz/-/issues/1887 - /// - public void ToDotFile(string filename) - { - File.WriteAllText(filename, ToDotString()); - } + /// + /// Attributes with the empty string as default are not correctly exported. + /// https://gitlab.com/graphviz/graphviz/-/issues/1887 + /// + public void ToDotFile(string filename) + { + File.WriteAllText(filename, ToDotString()); + } - /// - /// Create and return a subgraph containing the given edges and their endpoints. - /// - public SubGraph AddSubgraphFromEdgeSet(string name, HashSet edges) - { - var result = SubGraph.GetOrCreate(this, name); - result.AddExisting(edges); - // Since subgraphs can contain edges independently of their endpoints, - // we need to add the endpoints explicitly. - result.AddExisting(edges.SelectMany(e => new[] { e.Tail(), e.Head() })); - return result; + /// + /// Create and return a subgraph containing the given edges and their endpoints. + /// + public SubGraph AddSubgraphFromEdgeSet(string name, HashSet edges) + { + var result = SubGraph.GetOrCreate(this, name); + result.AddExisting(edges); + // Since subgraphs can contain edges independently of their endpoints, + // we need to add the endpoints explicitly. + result.AddExisting(edges.SelectMany(e => new[] { e.Tail(), e.Head() })); + return result; + } + + /// + /// Create a subgraph consisting of nodes from the given nodes. + /// Edges are added to the result if both endpoints are among the nodes. + /// Subgraphs are added to the result if they have nodes in the given nodelist. + /// The names of the Subgraphs are of the form "name:subgraphname". + /// + /// Side effect: adds the returned subgraph (and its children) to self. + /// + public SubGraph AddSubgraphFromNodes(string name, IEnumerable nodes) + { + // Freeze the list of descendants, + // since we are going to add subgraphs while iterating over existing subgraphs + List descendants = Descendants().ToList(); + + SubGraph result = GetOrAddSubgraph(name); + foreach (var node in nodes) + result.AddExisting(node); + + Debug.Assert(result.Nodes().Count() == nodes.Count()); + + // All that remains to do is to patch up the result by adding edges and subgraphs + foreach (var node in result.Nodes()) + foreach (var edge in node.EdgesOut(this)) + if (result.Contains(edge.Head())) + result.AddExisting(edge); + + Debug.Assert(result.Nodes().Count() == nodes.Count()); + + // Iterate over the (frozen) existing subgraphs and add new filtered subgraphs + // in the same hierarchical position as their unfiltered counterparts. + foreach (var subgraph in descendants) + { + string filteredsubgraphname = name + ":" + subgraph.GetName(); + Debug.WriteLine("Adding filtered subgraph {0}", filteredsubgraphname); + Graph parent = subgraph.Parent(); + Graph filteredparent; + if (parent.Equals(this)) + filteredparent = result; + else + { + string parentname = name + ":" + parent.GetName(); + filteredparent = result.GetDescendantByName(parentname); + Debug.Assert(filteredparent != null); + } + + _ = filteredparent.AddSubgraphFilteredByNodes(filteredsubgraphname, subgraph, nodes); } - /// - /// Create a subgraph consisting of nodes from the given nodes. - /// Edges are added to the result if both endpoints are among the nodes. - /// Subgraphs are added to the result if they have nodes in the given nodelist. - /// The names of the Subgraphs are of the form "name:subgraphname". - /// - /// Side effect: adds the returned subgraph (and its children) to self. - /// - public SubGraph AddSubgraphFromNodes(string name, IEnumerable nodes) - { - // Freeze the list of descendants, - // since we are going to add subgraphs while iterating over existing subgraphs - List descendants = Descendants().ToList(); + Debug.Assert(result.Nodes().Count() == nodes.Count()); - SubGraph result = GetOrAddSubgraph(name); - foreach (var node in nodes) - result.AddExisting(node); + // Remove subgraphs again if they are empty + // Again, we have to freeze the descendants we are enumerating, since we are disposing on the fly + result.SafeDeleteSubgraphs(s => !s.Nodes().Any()); - Debug.Assert(result.Nodes().Count() == nodes.Count()); + Debug.Assert(result.Nodes().Count() == nodes.Count()); - // All that remains to do is to patch up the result by adding edges and subgraphs - foreach (var node in result.Nodes()) - foreach (var edge in node.EdgesOut(this)) - if (result.Contains(edge.Head())) - result.AddExisting(edge); + return result; + } - Debug.Assert(result.Nodes().Count() == nodes.Count()); + /// + /// Delete all subgraphs in self fulfilling the predicate, without running into AccessViolationExceptions. + /// + public void SafeDeleteSubgraphs(Func predicate) + { + // Everytime we delete something, we restart the loop. + // This is not efficient, but easy to implement for now. + bool work_to_do = true; - // Iterate over the (frozen) existing subgraphs and add new filtered subgraphs - // in the same hierarchical position as their unfiltered counterparts. - foreach (var subgraph in descendants) + while (work_to_do) + { + work_to_do = false; + foreach (var subgraph in Descendants()) { - string filteredsubgraphname = name + ":" + subgraph.GetName(); - Debug.WriteLine("Adding filtered subgraph {0}", filteredsubgraphname); - Graph parent = subgraph.Parent(); - Graph filteredparent; - if (parent.Equals(this)) - filteredparent = result; - else - { - string parentname = name + ":" + parent.GetName(); - filteredparent = result.GetDescendantByName(parentname); - Debug.Assert(filteredparent != null); - } - - _ = filteredparent.AddSubgraphFilteredByNodes(filteredsubgraphname, subgraph, nodes); + if (!predicate(subgraph)) continue; + subgraph.Delete(); + work_to_do = true; + break; } + } + } - Debug.Assert(result.Nodes().Count() == nodes.Count()); - - // Remove subgraphs again if they are empty - // Again, we have to freeze the descendants we are enumerating, since we are disposing on the fly - result.SafeDeleteSubgraphs(s => !s.Nodes().Any()); + /// + /// Add a plain subgraph with given name to self, containing the nodes that occur both in origin + /// and the filter. In other words, filter the origin subgraph by the filter subgraph on node-level. + /// Attributes are copied from origin to the result. + /// + /// Side effect: adds the returned subgraph to self. + /// + public SubGraph AddSubgraphFilteredByNodes(string name, SubGraph origin, IEnumerable filter) + { + SubGraph result = GetOrAddSubgraph(name); + foreach (var node in origin.Nodes().Where(filter.Contains)) + result.AddExisting(node); - Debug.Assert(result.Nodes().Count() == nodes.Count()); + _ = origin.CopyAttributesTo(result); + return result; + } - return result; - } + /// + /// Create a deepcopy of the graph as a new root graph. + /// All nodes, edges and subgraphs contained in self are copied. + /// + /// No side effects to self. + /// + /// + public RootGraph Clone(string resultname) + { + RootGraph result = RootGraph.CreateNew(GetGraphType(), resultname); + _ = CopyAttributesTo(result); + CloneInto(result); + result.UpdateMemoryPressure(); + return result; + } - /// - /// Delete all subgraphs in self fulfilling the predicate, without running into AccessViolationExceptions. - /// - public void SafeDeleteSubgraphs(Func predicate) + public void CloneInto(RootGraph target) + { + // Copy all nodes and edges + foreach (var node in Nodes()) { - // Everytime we delete something, we restart the loop. - // This is not efficient, but easy to implement for now. - bool work_to_do = true; + string nodename = node.GetName(); + Node newnode = target.GetOrAddNode(nodename); - while (work_to_do) + foreach (var edge in node.EdgesOut(this)) { - work_to_do = false; - foreach (var subgraph in Descendants()) - { - if (!predicate(subgraph)) continue; - subgraph.Delete(); - work_to_do = true; - break; - } + Node head = edge.Head(); + Debug.Assert(Contains(head)); + Node tail = edge.Tail(); + Debug.Assert(node.Equals(tail)); + string headname = head.GetName(); + Node newhead = target.GetOrAddNode(headname); + string tailname = tail.GetName(); + Node newtail = target.GetNode(tailname); + + string edgename = edge.GetName(); + Edge newedge = target.GetOrAddEdge(newtail, newhead, edgename); + _ = edge.CopyAttributesTo(newedge); } + _ = node.CopyAttributesTo(newnode); } - /// - /// Add a plain subgraph with given name to self, containing the nodes that occur both in origin - /// and the filter. In other words, filter the origin subgraph by the filter subgraph on node-level. - /// Attributes are copied from origin to the result. - /// - /// Side effect: adds the returned subgraph to self. - /// - public SubGraph AddSubgraphFilteredByNodes(string name, SubGraph origin, IEnumerable filter) + // Copy all subgraphs + foreach (var subgraph in Descendants()) { - SubGraph result = GetOrAddSubgraph(name); - foreach (var node in origin.Nodes().Where(filter.Contains)) - result.AddExisting(node); - - _ = origin.CopyAttributesTo(result); - return result; - } - - /// - /// Create a deepcopy of the graph as a new root graph. - /// All nodes, edges and subgraphs contained in self are copied. - /// - /// No side effects to self. - /// - /// - public RootGraph Clone(string resultname) - { - RootGraph result = RootGraph.CreateNew(GetGraphType(), resultname); - _ = CopyAttributesTo(result); - CloneInto(result); - result.UpdateMemoryPressure(); - return result; - } + string subgraphname = subgraph.GetName(); + Graph parent = subgraph.Parent(); + Graph newparent; + if (parent.Equals(this)) + newparent = target; + else + { + string parentname = parent.GetName(); + newparent = target.GetDescendantByName(parentname); + Debug.Assert(newparent != null); + } + SubGraph newsubgraph = newparent.GetOrAddSubgraph(subgraphname); + _ = subgraph.CopyAttributesTo(newsubgraph); - public void CloneInto(RootGraph target) - { - // Copy all nodes and edges - foreach (var node in Nodes()) + // Add the (already created) nodes and edges to newly created subgraph + foreach (var node in subgraph.Nodes()) { string nodename = node.GetName(); - Node newnode = target.GetOrAddNode(nodename); + Node newnode = target.GetNode(nodename); + Debug.Assert(newnode != null); + newsubgraph.AddExisting(newnode); - foreach (var edge in node.EdgesOut(this)) + foreach (var edge in node.EdgesOut(subgraph)) { Node head = edge.Head(); - Debug.Assert(Contains(head)); Node tail = edge.Tail(); Debug.Assert(node.Equals(tail)); + string headname = head.GetName(); - Node newhead = target.GetOrAddNode(headname); + Node newhead = target.GetNode(headname); string tailname = tail.GetName(); Node newtail = target.GetNode(tailname); string edgename = edge.GetName(); - Edge newedge = target.GetOrAddEdge(newtail, newhead, edgename); - _ = edge.CopyAttributesTo(newedge); + Edge newedge = target.GetEdge(newtail, newhead, edgename); + newsubgraph.AddExisting(newedge); } _ = node.CopyAttributesTo(newnode); } - - // Copy all subgraphs - foreach (var subgraph in Descendants()) - { - string subgraphname = subgraph.GetName(); - Graph parent = subgraph.Parent(); - Graph newparent; - if (parent.Equals(this)) - newparent = target; - else - { - string parentname = parent.GetName(); - newparent = target.GetDescendantByName(parentname); - Debug.Assert(newparent != null); - } - SubGraph newsubgraph = newparent.GetOrAddSubgraph(subgraphname); - _ = subgraph.CopyAttributesTo(newsubgraph); - - // Add the (already created) nodes and edges to newly created subgraph - foreach (var node in subgraph.Nodes()) - { - string nodename = node.GetName(); - Node newnode = target.GetNode(nodename); - Debug.Assert(newnode != null); - newsubgraph.AddExisting(newnode); - - foreach (var edge in node.EdgesOut(subgraph)) - { - Node head = edge.Head(); - Node tail = edge.Tail(); - Debug.Assert(node.Equals(tail)); - - string headname = head.GetName(); - Node newhead = target.GetNode(headname); - string tailname = tail.GetName(); - Node newtail = target.GetNode(tailname); - - string edgename = edge.GetName(); - Edge newedge = target.GetEdge(newtail, newhead, edgename); - newsubgraph.AddExisting(newedge); - } - _ = node.CopyAttributesTo(newnode); - } - } } + } - /// - /// Contract an edge into a newly created node with given target name. - /// The attributes of the endpoints are merged and copied to the target node, - /// with head attributes taking precedence over tail attributes. - /// - /// The end points of the given edge are removed, as well as the edge itself. - /// Then all the neighbours of both endpoints are attached to the target, - /// preserving direction and attributes. - /// The new edges will be added to the root graph. - /// If the graph is strict, no multiple edges will be added between nodes. - /// - /// target - public Node Contract(Edge edge, string targetname) - { - return Contract(edge.Head(), edge.Tail(), targetname); - } + /// + /// Contract an edge into a newly created node with given target name. + /// The attributes of the endpoints are merged and copied to the target node, + /// with head attributes taking precedence over tail attributes. + /// + /// The end points of the given edge are removed, as well as the edge itself. + /// Then all the neighbours of both endpoints are attached to the target, + /// preserving direction and attributes. + /// The new edges will be added to the root graph. + /// If the graph is strict, no multiple edges will be added between nodes. + /// + /// target + public Node Contract(Edge edge, string targetname) + { + return Contract(edge.Head(), edge.Tail(), targetname); + } - /// - /// Perform a node contraction (also: node identification) on two nodes. - /// The resulting node will have targetname as name. - /// The attributes of the endpoints are merged and copied to the target node, - /// with head attributes taking precedence over tail attributes. - /// - /// Both node1 and node2 will be removed from the graph. - /// Then all the neighbours of both endpoints are attached to the target, - /// preserving direction and attributes. - /// The new edges will be added to the root graph. - /// If the graph is strict, no multiple edges will be added between nodes. - /// - /// target - public Node Contract(Node node1, Node node2, string targetname) - { - Node target = MyRootGraph.GetOrAddNode(targetname); - _ = node1.CopyAttributesTo(target); - _ = node2.CopyAttributesTo(target); - Merge(node1, target); - Merge(node2, target); - return target; - } + /// + /// Perform a node contraction (also: node identification) on two nodes. + /// The resulting node will have targetname as name. + /// The attributes of the endpoints are merged and copied to the target node, + /// with head attributes taking precedence over tail attributes. + /// + /// Both node1 and node2 will be removed from the graph. + /// Then all the neighbours of both endpoints are attached to the target, + /// preserving direction and attributes. + /// The new edges will be added to the root graph. + /// If the graph is strict, no multiple edges will be added between nodes. + /// + /// target + public Node Contract(Node node1, Node node2, string targetname) + { + Node target = MyRootGraph.GetOrAddNode(targetname); + _ = node1.CopyAttributesTo(target); + _ = node2.CopyAttributesTo(target); + Merge(node1, target); + Merge(node2, target); + return target; + } - /// - /// Merge a node into a target node. - /// Basically, add the neighborhood of the node to the neighborhood of the target. - /// The merge node will be removed from the graph. - /// The new edges will be added to the root graph. - /// - /// If the graph is strict, no multiple edges will be added between nodes. - /// - /// If add_self_loops is true, edges between the merge node and the target node will be - /// added as self loops to the target node. Self loops that already exist as such are always added. - /// - public void Merge(Node merge, Node target, bool add_self_loops = false) - { - // .Edges() won't iterate twice over self loops - foreach (var e in merge.Edges()) - if (!e.IsBetween(merge, target) || add_self_loops) // Only add self loops if we want that - { - Node newtail = e.Tail().Equals(merge) ? target : e.Tail(); - Node newhead = e.Head().Equals(merge) ? target : e.Head(); - Edge newedge = MyRootGraph.GetOrAddEdge(newtail, newhead, e.GetName()); - int returncode = e.CopyAttributesTo(newedge); - // For some reason this may fail, even when the copying seems to have succeeded. - Debug.Assert(returncode == 0); - } + /// + /// Merge a node into a target node. + /// Basically, add the neighborhood of the node to the neighborhood of the target. + /// The merge node will be removed from the graph. + /// The new edges will be added to the root graph. + /// + /// If the graph is strict, no multiple edges will be added between nodes. + /// + /// If add_self_loops is true, edges between the merge node and the target node will be + /// added as self loops to the target node. Self loops that already exist as such are always added. + /// + public void Merge(Node merge, Node target, bool add_self_loops = false) + { + // .Edges() won't iterate twice over self loops + foreach (var e in merge.Edges()) + if (!e.IsBetween(merge, target) || add_self_loops) // Only add self loops if we want that + { + Node newtail = e.Tail().Equals(merge) ? target : e.Tail(); + Node newhead = e.Head().Equals(merge) ? target : e.Head(); + Edge newedge = MyRootGraph.GetOrAddEdge(newtail, newhead, e.GetName()); + int returncode = e.CopyAttributesTo(newedge); + // For some reason this may fail, even when the copying seems to have succeeded. + Debug.Assert(returncode == 0); + } - // The following will delete all edges connected to merge. - MyRootGraph.Delete(merge); - } + // The following will delete all edges connected to merge. + MyRootGraph.Delete(merge); + } - public bool IsCluster() - { - return GetName().StartsWith("cluster"); - } + public bool IsCluster() + { + return GetName().StartsWith("cluster"); + } - /// - /// Must be true for logical tails/heads to be used in drawing. - /// - public bool IsCompound() - { - return GetAttribute("compound") == "true"; - } + /// + /// Must be true for logical tails/heads to be used in drawing. + /// + public bool IsCompound() + { + return GetAttribute("compound") == "true"; + } - public SubGraph GetOrCreateCluster(string name) - { - string clustername = "cluster_" + name; - SubGraph gvCluster = GetOrAddSubgraph(clustername); - return gvCluster; - } + public SubGraph GetOrCreateCluster(string name) + { + string clustername = "cluster_" + name; + SubGraph gvCluster = GetOrAddSubgraph(clustername); + return gvCluster; + } - private static int _dummyNodeIdCounter = 0; - private int GetNextDummyNodeId() - { - return _dummyNodeIdCounter++; - } + private static int _dummyNodeIdCounter = 0; + private int GetNextDummyNodeId() + { + return _dummyNodeIdCounter++; + } - public Node CreateInvisibleDummyNode() - { - var result = GetOrAddNode("dummynode-" + GetNextDummyNodeId().ToString()); - result.MakeInvisible(); - return result; - } + public Node CreateInvisibleDummyNode() + { + var result = GetOrAddNode("dummynode-" + GetNextDummyNodeId().ToString()); + result.MakeInvisible(); + return result; + } - public Node CreateSmallInvisibleDummyNode() - { - var result = GetOrAddNode("dummynode-" + GetNextDummyNodeId().ToString()); - result.MakeInvisibleAndSmall(); - return result; - } + public Node CreateSmallInvisibleDummyNode() + { + var result = GetOrAddNode("dummynode-" + GetNextDummyNodeId().ToString()); + result.MakeInvisibleAndSmall(); + return result; + } - /// - /// Creates an invisble dummy node as landingpoint for the cluster. - /// - public Edge GetOrAddEdge(Node gvNode, SubGraph gvCluster, bool makeLandingSpace, string edgeName) - { - // If there are any edges to a cluster, we need the an invisble dummy node as endpoint, - // because Graphviz does not support edges to clusters. We make it invisible but still - // take it up some space because there needs to be space for the edge to land on the - // cluster. Otherwise the edge will overlap with other edges too much, because if the - // invisible node takes no space it will be squeezed against another node. - Node invisibleHead; - if (makeLandingSpace) - invisibleHead = gvCluster.CreateInvisibleDummyNode(); - else - invisibleHead = gvCluster.CreateSmallInvisibleDummyNode(); - var edge = GetOrAddEdge(gvNode, invisibleHead, edgeName); - edge.SetLogicalHead(gvCluster); - return edge; - } + /// + /// Creates an invisble dummy node as landingpoint for the cluster. + /// + public Edge GetOrAddEdge(Node gvNode, SubGraph gvCluster, bool makeLandingSpace, string edgeName) + { + // If there are any edges to a cluster, we need the an invisble dummy node as endpoint, + // because Graphviz does not support edges to clusters. We make it invisible but still + // take it up some space because there needs to be space for the edge to land on the + // cluster. Otherwise the edge will overlap with other edges too much, because if the + // invisible node takes no space it will be squeezed against another node. + Node invisibleHead; + if (makeLandingSpace) + invisibleHead = gvCluster.CreateInvisibleDummyNode(); + else + invisibleHead = gvCluster.CreateSmallInvisibleDummyNode(); + var edge = GetOrAddEdge(gvNode, invisibleHead, edgeName); + edge.SetLogicalHead(gvCluster); + return edge; + } - /// - /// Creates an invisble dummy node as landingpoint for the cluster. - /// - public Edge GetOrAddEdge(SubGraph gvCluster, Node gvNode, bool makeLandingSpace, string edgeName) - { - Node invisibleTail; - if (makeLandingSpace) - invisibleTail = gvCluster.CreateInvisibleDummyNode(); - else - invisibleTail = gvCluster.CreateSmallInvisibleDummyNode(); - var edge = GetOrAddEdge(invisibleTail, gvNode, edgeName); - edge.SetLogicalTail(gvCluster); - return edge; - } + /// + /// Creates an invisble dummy node as landingpoint for the cluster. + /// + public Edge GetOrAddEdge(SubGraph gvCluster, Node gvNode, bool makeLandingSpace, string edgeName) + { + Node invisibleTail; + if (makeLandingSpace) + invisibleTail = gvCluster.CreateInvisibleDummyNode(); + else + invisibleTail = gvCluster.CreateSmallInvisibleDummyNode(); + var edge = GetOrAddEdge(invisibleTail, gvNode, edgeName); + edge.SetLogicalTail(gvCluster); + return edge; + } - /// - /// Creates an invisble dummy node as landingpoint for the cluster. - /// - public Edge GetOrAddEdge(SubGraph gvClusterTail, SubGraph gvClusterHead, bool makeLandingSpace, string edgeName) + /// + /// Creates an invisble dummy node as landingpoint for the cluster. + /// + public Edge GetOrAddEdge(SubGraph gvClusterTail, SubGraph gvClusterHead, bool makeLandingSpace, string edgeName) + { + Node invisibleTail; + Node invisibleHead; + if (makeLandingSpace) { - Node invisibleTail; - Node invisibleHead; - if (makeLandingSpace) - { - invisibleTail = gvClusterTail.CreateInvisibleDummyNode(); - invisibleHead = gvClusterHead.CreateInvisibleDummyNode(); - } - else - { - invisibleTail = gvClusterTail.CreateSmallInvisibleDummyNode(); - invisibleHead = gvClusterHead.CreateSmallInvisibleDummyNode(); - } - var edge = GetOrAddEdge(invisibleTail, invisibleHead, edgeName); - edge.SetLogicalTail(gvClusterTail); - edge.SetLogicalHead(gvClusterHead); - return edge; + invisibleTail = gvClusterTail.CreateInvisibleDummyNode(); + invisibleHead = gvClusterHead.CreateInvisibleDummyNode(); } - - #region layout functions and attributes - - /// - /// Compute the layout in a separate process by calling dot.exe, and return a new graph, which is a copy of the old - /// graph with the xdot information added to it. - /// - public RootGraph CreateLayout(string engine = LayoutEngines.Dot) + else { - return GraphvizCommand.CreateLayout(this, engine: engine); + invisibleTail = gvClusterTail.CreateSmallInvisibleDummyNode(); + invisibleHead = gvClusterHead.CreateSmallInvisibleDummyNode(); } + var edge = GetOrAddEdge(invisibleTail, invisibleHead, edgeName); + edge.SetLogicalTail(gvClusterTail); + edge.SetLogicalHead(gvClusterHead); + return edge; + } - public RectangleF GetBoundingBox() - { - string bb_string = Agget(_ptr, "bb"); - if (string.IsNullOrEmpty(bb_string)) - return default; - // x and y are the topleft point of the bb - char sep = ','; - string[] bb = bb_string.Split(sep); - float x = float.Parse(bb[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float y = float.Parse(bb[1], NumberStyles.Any, CultureInfo.InvariantCulture); - float w = float.Parse(bb[2], NumberStyles.Any, CultureInfo.InvariantCulture) - x; - float h = float.Parse(bb[3], NumberStyles.Any, CultureInfo.InvariantCulture) - y; - return new RectangleF(x, y, w, h); - } + #region layout functions and attributes - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + /// + /// Compute the layout in a separate process by calling dot.exe, and return a new graph, which is a copy of the old + /// graph with the xdot information added to it. + /// + public RootGraph CreateLayout(string engine = LayoutEngines.Dot) + { + return GraphvizCommand.CreateLayout(this, engine: engine); + } - private void ToFile(string filepath, string format, string engine) - { - _ = GraphvizCommand.Exec(this, format: format, filepath, engine: engine); - } + public RectangleF GetBoundingBox() + { + string bb_string = Agget(_ptr, "bb"); + if (string.IsNullOrEmpty(bb_string)) + return default; + // x and y are the topleft point of the bb + char sep = ','; + string[] bb = bb_string.Split(sep); + float x = float.Parse(bb[0], NumberStyles.Any, CultureInfo.InvariantCulture); + float y = float.Parse(bb[1], NumberStyles.Any, CultureInfo.InvariantCulture); + float w = float.Parse(bb[2], NumberStyles.Any, CultureInfo.InvariantCulture) - x; + float h = float.Parse(bb[3], NumberStyles.Any, CultureInfo.InvariantCulture) - y; + return new RectangleF(x, y, w, h); + } - public void ToSvgFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "svg", engine); - public void ToPngFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "png", engine); - public void ToPdfFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "pdf", engine); - public void ToPsFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "ps", engine); - #endregion + public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); + public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + private void ToFile(string filepath, string format, string engine) + { + _ = GraphvizCommand.Exec(this, format: format, filepath, engine: engine); + } - #region in-place layout computation + public void ToSvgFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "svg", engine); + public void ToPngFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "png", engine); + public void ToPdfFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "pdf", engine); + public void ToPsFile(string filepath, string engine = LayoutEngines.Dot) => ToFile(filepath, "ps", engine); + #endregion - /// - /// Compute a layout for this graph, in-process, on the given graph. - /// It is recommended to use instead, as that comes with less footguns and a better API. - /// Moreover, experience shows it is less likely to trip over lingering graphviz bugs as well. - /// NB: The method FreeLayout should always be called as soon as the layout information - /// of a graph is not needed anymore. - /// - public void ComputeLayout(string engine = LayoutEngines.Dot) - { - int layout_rc = GvLayout(GVC, _ptr, engine); - if (layout_rc != 0) - throw new ApplicationException($"Graphviz layout returned error code {layout_rc}"); - - // Calling gvRender this way sets attributes to the graph etc - // The engine specified here doesn't have to be the same as the above. - // We always want to use xdot here, independently of the layout algorithm, - // to ensure a consistent attribute layout. - int render_rc = GvRender(GVC, _ptr, "xdot", IntPtr.Zero); - if (render_rc != 0) - throw new ApplicationException($"Graphviz render returned error code {render_rc}"); - } - /// - /// Clean up the layout information stored in this graph. This does not include the attributes set by GvRender. - /// This method should always be called as soon as the layout information of a graph is not needed anymore. - /// NB: this method must not be called after modifications to the graph have been made! - /// This could result an AccessViolationException. - /// - public void FreeLayout() - { - var free_rc = GvFreeLayout(GVC, _ptr); - if (free_rc != 0) - throw new ApplicationException($"Graphviz render returned error code {free_rc}"); - } + #region in-place layout computation - /// - /// Should only be called after has been called. - /// - [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by the other ToXXXFile methods.")] - public void RenderToFile(string filename, string format) - { - var render_rc = GvRenderFilename(GVC, _ptr, format, filename); - if (render_rc != 0) - throw new ApplicationException($"Graphviz render returned error code {render_rc}"); - } + /// + /// Compute a layout for this graph, in-process, on the given graph. + /// It is recommended to use instead, as that comes with less footguns and a better API. + /// Moreover, experience shows it is less likely to trip over lingering graphviz bugs as well. + /// NB: The method FreeLayout should always be called as soon as the layout information + /// of a graph is not needed anymore. + /// + public void ComputeLayout(string engine = LayoutEngines.Dot) + { + int layout_rc = GvLayout(GVC, _ptr, engine); + if (layout_rc != 0) + throw new ApplicationException($"Graphviz layout returned error code {layout_rc}"); + + // Calling gvRender this way sets attributes to the graph etc + // The engine specified here doesn't have to be the same as the above. + // We always want to use xdot here, independently of the layout algorithm, + // to ensure a consistent attribute layout. + int render_rc = GvRender(GVC, _ptr, "xdot", IntPtr.Zero); + if (render_rc != 0) + throw new ApplicationException($"Graphviz render returned error code {render_rc}"); + } - [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] - public GraphvizLabel GetLabel() - { - IntPtr labelptr = GraphLabel(_ptr); - if (labelptr == IntPtr.Zero) - return null; - return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered); - } + /// + /// Clean up the layout information stored in this graph. This does not include the attributes set by GvRender. + /// This method should always be called as soon as the layout information of a graph is not needed anymore. + /// NB: this method must not be called after modifications to the graph have been made! + /// This could result an AccessViolationException. + /// + public void FreeLayout() + { + var free_rc = GvFreeLayout(GVC, _ptr); + if (free_rc != 0) + throw new ApplicationException($"Graphviz render returned error code {free_rc}"); + } + + /// + /// Should only be called after has been called. + /// + [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by the other ToXXXFile methods.")] + public void RenderToFile(string filename, string format) + { + var render_rc = GvRenderFilename(GVC, _ptr, format, filename); + if (render_rc != 0) + throw new ApplicationException($"Graphviz render returned error code {render_rc}"); + } - #endregion + [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] + public GraphvizLabel GetLabel() + { + IntPtr labelptr = GraphLabel(_ptr); + if (labelptr == IntPtr.Zero) + return null; + return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered); } + + #endregion } diff --git a/Rubjerg.Graphviz/GraphComparer.cs b/Rubjerg.Graphviz/GraphComparer.cs index 77bfab2..6657d50 100644 --- a/Rubjerg.Graphviz/GraphComparer.cs +++ b/Rubjerg.Graphviz/GraphComparer.cs @@ -2,96 +2,95 @@ using System.Collections.Generic; using System.Linq; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public static class GraphComparer { - public static class GraphComparer + public static bool CheckTopologicallyEquals(Graph A, Graph B, Action logger) { - public static bool CheckTopologicallyEquals(Graph A, Graph B, Action logger) - { - logger($"Comparing graph A = '{A.GetName()}' with graph B = '{B.GetName()}'"); - logger(""); - - bool result = true; - var common_nodenames = new List(); - foreach (var node in A.Nodes()) - { - var othernode = B.GetNode(node.GetName()); - if (othernode == null) - { - logger($"graph B does not contain node {node.GetName()}"); - result = false; - continue; - } - common_nodenames.Add(node.GetName()); - } + logger($"Comparing graph A = '{A.GetName()}' with graph B = '{B.GetName()}'"); + logger(""); - foreach (var node in B.Nodes()) + bool result = true; + var common_nodenames = new List(); + foreach (var node in A.Nodes()) + { + var othernode = B.GetNode(node.GetName()); + if (othernode == null) { - var othernode = A.GetNode(node.GetName()); - if (othernode == null) - { - logger($"graph A does not contain node {node.GetName()}"); - result = false; - } + logger($"graph B does not contain node {node.GetName()}"); + result = false; + continue; } + common_nodenames.Add(node.GetName()); + } - foreach (var nodename in common_nodenames) + foreach (var node in B.Nodes()) + { + var othernode = A.GetNode(node.GetName()); + if (othernode == null) { - result &= CheckNode(A, B, A.GetNode(nodename), B.GetNode(nodename), logger); + logger($"graph A does not contain node {node.GetName()}"); + result = false; } - - logger(""); - logger($"A and B are {(result ? "" : "NOT")} topologically equivalent"); - return result; } - private static bool CheckNode(Graph A, Graph B, Node nA, Node nB, Action logger) + foreach (var nodename in common_nodenames) { - return InnerCheckNode(A, B, nA, nB, logger, "B") & InnerCheckNode(B, A, nA, nB, logger, "A"); + result &= CheckNode(A, B, A.GetNode(nodename), B.GetNode(nodename), logger); } - private static bool InnerCheckNode(Graph A, Graph B, Node nA, Node nB, Action logger, string nameOfGraphOfNodeB) + logger(""); + logger($"A and B are {(result ? "" : "NOT")} topologically equivalent"); + return result; + } + + private static bool CheckNode(Graph A, Graph B, Node nA, Node nB, Action logger) + { + return InnerCheckNode(A, B, nA, nB, logger, "B") & InnerCheckNode(B, A, nA, nB, logger, "A"); + } + + private static bool InnerCheckNode(Graph A, Graph B, Node nA, Node nB, Action logger, string nameOfGraphOfNodeB) + { + bool result = true; + foreach (var eA in nA.EdgesOut(A)) { - bool result = true; - foreach (var eA in nA.EdgesOut(A)) + var expected_endpoint = eA.OppositeEndpoint(nA); + bool diff = false; + if (!nB.EdgesOut(B).Any(eB => CheckEdgeName(eA, eB))) { - var expected_endpoint = eA.OppositeEndpoint(nA); - bool diff = false; - if (!nB.EdgesOut(B).Any(eB => CheckEdgeName(eA, eB))) - { - logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with name '{eA.GetName()}'"); - result = false; - diff = true; - } - if (!nB.EdgesOut(B).Any(eB => CheckEdgeEndpoints(eA, eB))) - { - logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with head '{expected_endpoint.GetName()}'"); - result = false; - diff = true; - } - if (!diff && !nB.EdgesOut(B).Any(eB => CheckEdge(eA, eB))) - { - logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with **both** name '{eA.GetName()}' and head '{expected_endpoint.GetName()}'"); - result = false; - } + logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with name '{eA.GetName()}'"); + result = false; + diff = true; + } + if (!nB.EdgesOut(B).Any(eB => CheckEdgeEndpoints(eA, eB))) + { + logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with head '{expected_endpoint.GetName()}'"); + result = false; + diff = true; + } + if (!diff && !nB.EdgesOut(B).Any(eB => CheckEdge(eA, eB))) + { + logger($@"In graph {nameOfGraphOfNodeB} the node '{nB.GetName()}' does not have an outgoing edge with **both** name '{eA.GetName()}' and head '{expected_endpoint.GetName()}'"); + result = false; } - return result; - } - - public static bool CheckEdge(Edge eA, Edge eB) - { - return CheckEdgeName(eA, eB) && CheckEdgeEndpoints(eA, eB); } + return result; + } - public static bool CheckEdgeName(Edge eA, Edge eB) - { - return eA.GetName() == eB.GetName(); - } + public static bool CheckEdge(Edge eA, Edge eB) + { + return CheckEdgeName(eA, eB) && CheckEdgeEndpoints(eA, eB); + } - public static bool CheckEdgeEndpoints(Edge eA, Edge eB) - { - return eA.Head().GetName() == eB.Head().GetName() && eA.Tail().GetName() == eB.Tail().GetName(); - } + public static bool CheckEdgeName(Edge eA, Edge eB) + { + return eA.GetName() == eB.GetName(); + } + public static bool CheckEdgeEndpoints(Edge eA, Edge eB) + { + return eA.Head().GetName() == eB.Head().GetName() && eA.Tail().GetName() == eB.Tail().GetName(); } + } diff --git a/Rubjerg.Graphviz/GraphVizLabel.cs b/Rubjerg.Graphviz/GraphVizLabel.cs index fc4588b..0a69b52 100644 --- a/Rubjerg.Graphviz/GraphVizLabel.cs +++ b/Rubjerg.Graphviz/GraphVizLabel.cs @@ -2,68 +2,67 @@ using System.Drawing; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +/// +/// In Graphviz the way coordinates of bounding boxes are represented may differ. +/// We want to provide a uniform API with bottom left coords only, so we use this enum to +/// keep track of the current internal representation and convert if needed. +/// +internal enum BoundingBoxCoords { + Centered, + BottomLeft +} + +/// +/// Wraps a graphviz label for any kind of graphviz object. +/// +[Obsolete("This object is only available after ComputeLayout(). It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] +public class GraphvizLabel : GraphvizThing +{ + private readonly BoundingBoxCoords representation; + private readonly PointF offset; + /// - /// In Graphviz the way coordinates of bounding boxes are represented may differ. - /// We want to provide a uniform API with bottom left coords only, so we use this enum to - /// keep track of the current internal representation and convert if needed. + /// Unfortunately the way the bounding box is stored differs per object that the label belongs to. + /// Therefore some extra information is needed to uniformly define a Label object. /// - internal enum BoundingBoxCoords + internal GraphvizLabel(IntPtr ptr, BoundingBoxCoords representation, PointF offset = default) + : base(ptr) { - Centered, - BottomLeft + this.representation = representation; + this.offset = offset; + } + + public string FontName() + { + return LabelFontname(_ptr); } /// - /// Wraps a graphviz label for any kind of graphviz object. + /// Label size in points. /// - [Obsolete("This object is only available after ComputeLayout(). It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] - public class GraphvizLabel : GraphvizThing + public float FontSize() { - private readonly BoundingBoxCoords representation; - private readonly PointF offset; - - /// - /// Unfortunately the way the bounding box is stored differs per object that the label belongs to. - /// Therefore some extra information is needed to uniformly define a Label object. - /// - internal GraphvizLabel(IntPtr ptr, BoundingBoxCoords representation, PointF offset = default) - : base(ptr) - { - this.representation = representation; - this.offset = offset; - } - - public string FontName() - { - return LabelFontname(_ptr); - } - - /// - /// Label size in points. - /// - public float FontSize() - { - return Convert.ToSingle(LabelFontsize(_ptr)); - } - - public string Text() - { - return LabelText(_ptr); - } + return Convert.ToSingle(LabelFontsize(_ptr)); + } - public RectangleF BoundingBox() - { - float x = Convert.ToSingle(LabelX(_ptr)) + offset.X; - float y = Convert.ToSingle(LabelY(_ptr)) + offset.Y; - float w = Convert.ToSingle(LabelWidth(_ptr)); - float h = Convert.ToSingle(LabelHeight(_ptr)); - if (representation == BoundingBoxCoords.Centered) - return new RectangleF(x - w / 2, y - h / 2, w, h); - else - return new RectangleF(x, y, w, h); - } + public string Text() + { + return LabelText(_ptr); + } + public RectangleF BoundingBox() + { + float x = Convert.ToSingle(LabelX(_ptr)) + offset.X; + float y = Convert.ToSingle(LabelY(_ptr)) + offset.Y; + float w = Convert.ToSingle(LabelWidth(_ptr)); + float h = Convert.ToSingle(LabelHeight(_ptr)); + if (representation == BoundingBoxCoords.Centered) + return new RectangleF(x - w / 2, y - h / 2, w, h); + else + return new RectangleF(x, y, w, h); } + } diff --git a/Rubjerg.Graphviz/GraphVizThing.cs b/Rubjerg.Graphviz/GraphVizThing.cs index 72ef211..3da813e 100644 --- a/Rubjerg.Graphviz/GraphVizThing.cs +++ b/Rubjerg.Graphviz/GraphVizThing.cs @@ -1,54 +1,53 @@ using System; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +/// +/// This is the most basic entity for our graphviz wrapper. It wraps a C pointer to a managed +/// resource, and wraps C functions with object oriented methods. Everything that is wrapped, +/// derives from this. Since a graphviz thing is a dumb wrapper around a managed pointer, there +/// can be multiple wrappers for the same pointer. Ideally we would want to be a simple struct, +/// but structs can't be subclassed in C#, so we must be a class and live with the +/// overhead in code and performance. This implies that we need to override what it means for +/// two wrappers to be equal (i.e. not reference equality for wrappers, but equality of the +/// pointers they wrap) to allow usage of common functions (like linq contains) in a way that +/// makes sense. +/// +/// Invariant: ptr member is never 0. +/// +public abstract class GraphvizThing { - /// - /// This is the most basic entity for our graphviz wrapper. It wraps a C pointer to a managed - /// resource, and wraps C functions with object oriented methods. Everything that is wrapped, - /// derives from this. Since a graphviz thing is a dumb wrapper around a managed pointer, there - /// can be multiple wrappers for the same pointer. Ideally we would want to be a simple struct, - /// but structs can't be subclassed in C#, so we must be a class and live with the - /// overhead in code and performance. This implies that we need to override what it means for - /// two wrappers to be equal (i.e. not reference equality for wrappers, but equality of the - /// pointers they wrap) to allow usage of common functions (like linq contains) in a way that - /// makes sense. - /// - /// Invariant: ptr member is never 0. - /// - public abstract class GraphvizThing + internal readonly IntPtr _ptr; + + protected GraphvizThing(IntPtr ptr) + { + if (ptr == IntPtr.Zero) + throw new ArgumentException("Can't have a null pointer."); + _ptr = ptr; + } + + public override bool Equals(object obj) + { + return Equals(obj as GraphvizThing); + } + + public virtual bool Equals(GraphvizThing obj) + { + return obj != null && _ptr == obj._ptr; + } + + public override int GetHashCode() + { + return _ptr.GetHashCode(); + } + + public static bool operator ==(GraphvizThing a, GraphvizThing b) + { + return Equals(a, b); + } + + public static bool operator !=(GraphvizThing a, GraphvizThing b) { - internal readonly IntPtr _ptr; - - protected GraphvizThing(IntPtr ptr) - { - if (ptr == IntPtr.Zero) - throw new ArgumentException("Can't have a null pointer."); - _ptr = ptr; - } - - public override bool Equals(object obj) - { - return Equals(obj as GraphvizThing); - } - - public virtual bool Equals(GraphvizThing obj) - { - return obj != null && _ptr == obj._ptr; - } - - public override int GetHashCode() - { - return _ptr.GetHashCode(); - } - - public static bool operator ==(GraphvizThing a, GraphvizThing b) - { - return Equals(a, b); - } - - public static bool operator !=(GraphvizThing a, GraphvizThing b) - { - return !(a == b); - } + return !(a == b); } } diff --git a/Rubjerg.Graphviz/GraphvizCommand.cs b/Rubjerg.Graphviz/GraphvizCommand.cs index b7665d4..9d7e872 100644 --- a/Rubjerg.Graphviz/GraphvizCommand.cs +++ b/Rubjerg.Graphviz/GraphvizCommand.cs @@ -2,77 +2,76 @@ using System.Diagnostics; using System.IO; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +/// +/// See https://graphviz.org/doc/info/command.html +/// +public class GraphvizCommand { - /// - /// See https://graphviz.org/doc/info/command.html - /// - public class GraphvizCommand + public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot) { - public static RootGraph CreateLayout(Graph input, string engine = LayoutEngines.Dot) - { - var output = Exec(input, engine: engine); - var resultGraph = RootGraph.FromDotString(output); - return resultGraph; - } + var output = Exec(input, engine: engine); + var resultGraph = RootGraph.FromDotString(output); + return resultGraph; + } - public static string Exec(Graph input, string format = "xdot", string outputPath = null, string engine = LayoutEngines.Dot) + public static string Exec(Graph input, string format = "xdot", string outputPath = null, string engine = LayoutEngines.Dot) + { + string exeName = "dot.exe"; + string arguments = $"-T{format} -K{engine}"; + if (outputPath != null) { - string exeName = "dot.exe"; - string arguments = $"-T{format} -K{engine}"; - if (outputPath != null) - { - arguments = $"{arguments} -o{outputPath}"; - } - string inputToStdin = input.ToDotString(); + arguments = $"{arguments} -o{outputPath}"; + } + string inputToStdin = input.ToDotString(); - // Get the location of the currently executing DLL - string exeDirectory = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; + // Get the location of the currently executing DLL + string exeDirectory = AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; - // Construct the path to the executable - string exePath = Path.Combine(exeDirectory, exeName); + // Construct the path to the executable + string exePath = Path.Combine(exeDirectory, exeName); - Process process = new Process(); + Process process = new Process(); - process.StartInfo.FileName = exePath; - process.StartInfo.Arguments = arguments; + process.StartInfo.FileName = exePath; + process.StartInfo.Arguments = arguments; - // Redirect the input/output streams - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardInput = true; - process.StartInfo.RedirectStandardError = true; + // Redirect the input/output streams + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardError = true; - _ = process.Start(); + _ = process.Start(); - // Write to stdin - using (StreamWriter sw = process.StandardInput) - sw.WriteLine(inputToStdin); + // Write to stdin + using (StreamWriter sw = process.StandardInput) + sw.WriteLine(inputToStdin); - // Read from stdout - string output; - using (StreamReader sr = process.StandardOutput) - output = sr.ReadToEnd() - .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not + // Read from stdout + string output; + using (StreamReader sr = process.StandardOutput) + output = sr.ReadToEnd() + .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not - // Read from stderr - string error; - using (StreamReader sr = process.StandardError) - error = sr.ReadToEnd() - .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not + // Read from stderr + string error; + using (StreamReader sr = process.StandardError) + error = sr.ReadToEnd() + .Replace("\r\n", "\n"); // File operations do this automatically, but stream operations do not - process.WaitForExit(); + process.WaitForExit(); - if (process.ExitCode != 0) - { - // Something went wrong. - throw new ApplicationException($"Process exited with code {process.ExitCode}. Error details: {error}"); - } - else - { - // Process completed successfully. - return output; - } + if (process.ExitCode != 0) + { + // Something went wrong. + throw new ApplicationException($"Process exited with code {process.ExitCode}. Error details: {error}"); + } + else + { + // Process completed successfully. + return output; } } } diff --git a/Rubjerg.Graphviz/LayoutEngines.cs b/Rubjerg.Graphviz/LayoutEngines.cs index 9bfddb1..0b50cf1 100644 --- a/Rubjerg.Graphviz/LayoutEngines.cs +++ b/Rubjerg.Graphviz/LayoutEngines.cs @@ -1,14 +1,13 @@ -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public static class LayoutEngines { - public static class LayoutEngines - { - public const string Dot = "dot"; - public const string Neato = "neato"; - public const string Fdp = "fdp"; - public const string Sfdp = "sfdp"; - public const string Twopi = "twopi"; - public const string Circo = "circo"; - public const string Patchwork = "patchwork"; - public const string Osage = "osage"; - } + public const string Dot = "dot"; + public const string Neato = "neato"; + public const string Fdp = "fdp"; + public const string Sfdp = "sfdp"; + public const string Twopi = "twopi"; + public const string Circo = "circo"; + public const string Patchwork = "patchwork"; + public const string Osage = "osage"; } diff --git a/Rubjerg.Graphviz/NativeMethods.cs b/Rubjerg.Graphviz/NativeMethods.cs index 74f6c49..aaf766f 100644 --- a/Rubjerg.Graphviz/NativeMethods.cs +++ b/Rubjerg.Graphviz/NativeMethods.cs @@ -2,35 +2,34 @@ using System.IO; using System.Runtime.InteropServices; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public static class NativeMethods { - public static class NativeMethods + public static void CreateConsole() { - public static void CreateConsole() - { - _ = AllocConsole(); - - // stdout's handle seems to always be equal to 7 - IntPtr defaultStdout = new IntPtr(7); - IntPtr currentStdout = GetStdHandle(StdOutputHandle); + _ = AllocConsole(); - if (currentStdout != defaultStdout) - // reset stdout - SetStdHandle(StdOutputHandle, defaultStdout); + // stdout's handle seems to always be equal to 7 + IntPtr defaultStdout = new IntPtr(7); + IntPtr currentStdout = GetStdHandle(StdOutputHandle); - // reopen stdout - TextWriter writer = new StreamWriter(Console.OpenStandardOutput()) - { AutoFlush = true }; - Console.SetOut(writer); - } + if (currentStdout != defaultStdout) + // reset stdout + SetStdHandle(StdOutputHandle, defaultStdout); - // P/Invoke required: - private const uint StdOutputHandle = 0xFFFFFFF5; - [DllImport("kernel32.dll")] - private static extern IntPtr GetStdHandle(uint nStdHandle); - [DllImport("kernel32.dll")] - private static extern void SetStdHandle(uint nStdHandle, IntPtr handle); - [DllImport("kernel32")] - static extern bool AllocConsole(); + // reopen stdout + TextWriter writer = new StreamWriter(Console.OpenStandardOutput()) + { AutoFlush = true }; + Console.SetOut(writer); } + + // P/Invoke required: + private const uint StdOutputHandle = 0xFFFFFFF5; + [DllImport("kernel32.dll")] + private static extern IntPtr GetStdHandle(uint nStdHandle); + [DllImport("kernel32.dll")] + private static extern void SetStdHandle(uint nStdHandle, IntPtr handle); + [DllImport("kernel32")] + static extern bool AllocConsole(); } diff --git a/Rubjerg.Graphviz/Node.cs b/Rubjerg.Graphviz/Node.cs index 39eaa41..8783816 100644 --- a/Rubjerg.Graphviz/Node.cs +++ b/Rubjerg.Graphviz/Node.cs @@ -5,284 +5,283 @@ using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public class Node : CGraphThing { - public class Node : CGraphThing + /// + /// rootgraph must not be null + /// + internal Node(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } + + internal static Node Get(Graph graph, string name) { - /// - /// rootgraph must not be null - /// - internal Node(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } + name = NameString(name); + IntPtr ptr = Agnode(graph._ptr, name, 0); + if (ptr != IntPtr.Zero) + return new Node(ptr, graph.MyRootGraph); + return null; + } - internal static Node Get(Graph graph, string name) - { - name = NameString(name); - IntPtr ptr = Agnode(graph._ptr, name, 0); - if (ptr != IntPtr.Zero) - return new Node(ptr, graph.MyRootGraph); - return null; - } + internal static Node GetOrCreate(Graph graph, string name) + { + name = NameString(name); + IntPtr ptr = Agnode(graph._ptr, name, 1); + return new Node(ptr, graph.MyRootGraph); + } - internal static Node GetOrCreate(Graph graph, string name) - { - name = NameString(name); - IntPtr ptr = Agnode(graph._ptr, name, 1); - return new Node(ptr, graph.MyRootGraph); - } + /// + /// Introduces an attribute for nodes in the given graph by giving a default value. + /// A given default can be overwritten by calling this method again. + /// + public static void IntroduceAttribute(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + Agattr(root._ptr, 1, name, deflt); + } - /// - /// Introduces an attribute for nodes in the given graph by giving a default value. - /// A given default can be overwritten by calling this method again. - /// - public static void IntroduceAttribute(RootGraph root, string name, string deflt) - { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - Agattr(root._ptr, 1, name, deflt); - } + public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) + { + _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); + AgattrHtml(root._ptr, 1, name, deflt); + } - public static void IntroduceAttributeHtml(RootGraph root, string name, string deflt) + public IEnumerable EdgesOut(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + var current = Agfstout(graph_ptr, _ptr); + while (current != IntPtr.Zero) { - _ = deflt ?? throw new ArgumentNullException(nameof(deflt)); - AgattrHtml(root._ptr, 1, name, deflt); + yield return new Edge(current, MyRootGraph); + current = Agnxtout(graph_ptr, current); } + } - public IEnumerable EdgesOut(Graph graph = null) + public IEnumerable EdgesIn(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + var current = Agfstin(graph_ptr, _ptr); + while (current != IntPtr.Zero) { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - var current = Agfstout(graph_ptr, _ptr); - while (current != IntPtr.Zero) - { - yield return new Edge(current, MyRootGraph); - current = Agnxtout(graph_ptr, current); - } + yield return new Edge(current, MyRootGraph); + current = Agnxtin(graph_ptr, current); } + } - public IEnumerable EdgesIn(Graph graph = null) + /// + /// Iterate over both in and out edges. This will not yield self loops twice. + /// + public IEnumerable Edges(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + var current = Agfstedge(graph_ptr, _ptr); + while (current != IntPtr.Zero) { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - var current = Agfstin(graph_ptr, _ptr); - while (current != IntPtr.Zero) - { - yield return new Edge(current, MyRootGraph); - current = Agnxtin(graph_ptr, current); - } + yield return new Edge(current, MyRootGraph); + current = Agnxtedge(graph_ptr, current, _ptr); // This line crashes at some point } + } - /// - /// Iterate over both in and out edges. This will not yield self loops twice. - /// - public IEnumerable Edges(Graph graph = null) - { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - var current = Agfstedge(graph_ptr, _ptr); - while (current != IntPtr.Zero) - { - yield return new Edge(current, MyRootGraph); - current = Agnxtedge(graph_ptr, current, _ptr); // This line crashes at some point - } - } + /// + /// Get all neighbors connected via an out edge. + /// + public IEnumerable NeighborsOut(Graph graph = null) + { + return EdgesOut(graph).Select(e => e.OppositeEndpoint(this)); + } - /// - /// Get all neighbors connected via an out edge. - /// - public IEnumerable NeighborsOut(Graph graph = null) - { - return EdgesOut(graph).Select(e => e.OppositeEndpoint(this)); - } + /// + /// Get all neighbors connected via an in edge. + /// + public IEnumerable NeighborsIn(Graph graph = null) + { + return EdgesIn(graph).Select(e => e.OppositeEndpoint(this)); + } - /// - /// Get all neighbors connected via an in edge. - /// - public IEnumerable NeighborsIn(Graph graph = null) - { - return EdgesIn(graph).Select(e => e.OppositeEndpoint(this)); - } + /// + /// Get all neighbors. + /// + public IEnumerable Neighbors(Graph graph = null) + { + return Edges(graph).Select(e => e.OppositeEndpoint(this)); + } - /// - /// Get all neighbors. - /// - public IEnumerable Neighbors(Graph graph = null) - { - return Edges(graph).Select(e => e.OppositeEndpoint(this)); - } + /// + /// Get all neighbors fullfilling a given attribute constraint. + /// + public IEnumerable NeighborsByAttribute(string attr_name, string attr_value, Graph graph = null) + { + return Neighbors(graph).Where(n => n.GetAttribute(attr_name) == attr_value); + } - /// - /// Get all neighbors fullfilling a given attribute constraint. - /// - public IEnumerable NeighborsByAttribute(string attr_name, string attr_value, Graph graph = null) - { - return Neighbors(graph).Where(n => n.GetAttribute(attr_name) == attr_value); - } + /// + /// Get all neighbors connected by an edge with given name. + /// + public IEnumerable NeighborsByEdgeName(string edgename, Graph graph = null) + { + return Edges(graph).Where(e => e.GetName() == edgename).Select(e => e.OppositeEndpoint(this)); + } - /// - /// Get all neighbors connected by an edge with given name. - /// - public IEnumerable NeighborsByEdgeName(string edgename, Graph graph = null) - { - return Edges(graph).Where(e => e.GetName() == edgename).Select(e => e.OppositeEndpoint(this)); - } + /// + /// Copy the node to another root graph. + /// Copies the attributes as well, as far as the attributes have been + /// introduced in the destination graph. + /// + public Node CopyToOtherRoot(RootGraph destination) + { + Node result = destination.GetOrAddNode(GetName()); + _ = CopyAttributesTo(result); + return result; + } - /// - /// Copy the node to another root graph. - /// Copies the attributes as well, as far as the attributes have been - /// introduced in the destination graph. - /// - public Node CopyToOtherRoot(RootGraph destination) - { - Node result = destination.GetOrAddNode(GetName()); - _ = CopyAttributesTo(result); - return result; - } + public int OutDegree(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + return Agdegree(graph_ptr, _ptr, 0, 1); + } - public int OutDegree(Graph graph = null) - { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - return Agdegree(graph_ptr, _ptr, 0, 1); - } + public int InDegree(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + return Agdegree(graph_ptr, _ptr, 1, 0); + } - public int InDegree(Graph graph = null) - { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - return Agdegree(graph_ptr, _ptr, 1, 0); - } + public int TotalDegree(Graph graph = null) + { + IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; + return Agdegree(graph_ptr, _ptr, 1, 1); + } - public int TotalDegree(Graph graph = null) - { - IntPtr graph_ptr = graph?._ptr ?? MyRootGraph._ptr; - return Agdegree(graph_ptr, _ptr, 1, 1); - } + public bool IsAdjacentTo(Node node) + { + return EdgesOut().Any(e => e.Head().Equals(node)) || EdgesIn().Any(e => e.Tail().Equals(node)); + } + + public void MakeInvisibleAndSmall() + { + SafeSetAttribute("style", "invis", ""); + SafeSetAttribute("margin", "0", ""); + SafeSetAttribute("width", "0", ""); + SafeSetAttribute("height", "0", ""); + SafeSetAttribute("shape", "point", ""); + } + + #region layout attributes - public bool IsAdjacentTo(Node node) + /// + /// The position of the center of the node. + /// + public PointF GetPosition() + { + // The "pos" attribute is available as part of xdot output + if (HasAttribute("pos")) { - return EdgesOut().Any(e => e.Head().Equals(node)) || EdgesIn().Any(e => e.Tail().Equals(node)); + var posString = GetAttribute("pos"); + var coords = posString.Split(','); + float x = float.Parse(coords[0], NumberStyles.Any, CultureInfo.InvariantCulture); + float y = float.Parse(coords[1], NumberStyles.Any, CultureInfo.InvariantCulture); + return new PointF(x, y); } + // If the "pos" attribute is not available, try the following FFI functions, + // which are available after a ComputeLayout + return new PointF(Convert.ToSingle(NodeX(_ptr)), Convert.ToSingle(NodeY(_ptr))); + } - public void MakeInvisibleAndSmall() + /// + /// The size of bounding box of the node. + /// + public SizeF GetSize() + { + // The "width" and "height" attributes are available as part of xdot output + float w, h; + if (HasAttribute("width") && HasAttribute("height")) { - SafeSetAttribute("style", "invis", ""); - SafeSetAttribute("margin", "0", ""); - SafeSetAttribute("width", "0", ""); - SafeSetAttribute("height", "0", ""); - SafeSetAttribute("shape", "point", ""); + w = float.Parse(GetAttribute("width"), NumberStyles.Any, CultureInfo.InvariantCulture); + h = float.Parse(GetAttribute("height"), NumberStyles.Any, CultureInfo.InvariantCulture); } - - #region layout attributes - - /// - /// The position of the center of the node. - /// - public PointF GetPosition() + else { - // The "pos" attribute is available as part of xdot output - if (HasAttribute("pos")) - { - var posString = GetAttribute("pos"); - var coords = posString.Split(','); - float x = float.Parse(coords[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float y = float.Parse(coords[1], NumberStyles.Any, CultureInfo.InvariantCulture); - return new PointF(x, y); - } - // If the "pos" attribute is not available, try the following FFI functions, + // If they are not available, try the following FFI functions, // which are available after a ComputeLayout - return new PointF(Convert.ToSingle(NodeX(_ptr)), Convert.ToSingle(NodeY(_ptr))); + w = Convert.ToSingle(NodeWidth(_ptr)); + h = Convert.ToSingle(NodeHeight(_ptr)); } + // Coords are in points, sizes in inches. 72 points = 1 inch + // We return everything in terms of points. + return new SizeF(w * 72, h * 72); + } - /// - /// The size of bounding box of the node. - /// - public SizeF GetSize() - { - // The "width" and "height" attributes are available as part of xdot output - float w, h; - if (HasAttribute("width") && HasAttribute("height")) - { - w = float.Parse(GetAttribute("width"), NumberStyles.Any, CultureInfo.InvariantCulture); - h = float.Parse(GetAttribute("height"), NumberStyles.Any, CultureInfo.InvariantCulture); - } - else - { - // If they are not available, try the following FFI functions, - // which are available after a ComputeLayout - w = Convert.ToSingle(NodeWidth(_ptr)); - h = Convert.ToSingle(NodeHeight(_ptr)); - } - // Coords are in points, sizes in inches. 72 points = 1 inch - // We return everything in terms of points. - return new SizeF(w * 72, h * 72); - } + public RectangleF GetBoundingBox() + { + var size = GetSize(); + var center = GetPosition(); + var bottomleft = new PointF(center.X - size.Width / 2, center.Y - size.Height / 2); + return new RectangleF(bottomleft, size); + } - public RectangleF GetBoundingBox() - { - var size = GetSize(); - var center = GetPosition(); - var bottomleft = new PointF(center.X - size.Width / 2, center.Y - size.Height / 2); - return new RectangleF(bottomleft, size); - } + /// + /// If the shape of this node was set to 'record', this method allows you to retrieve the + /// resulting rectangles. + /// + public IEnumerable GetRecordRectangles() + { + if (!HasAttribute("rects")) + yield break; - /// - /// If the shape of this node was set to 'record', this method allows you to retrieve the - /// resulting rectangles. - /// - public IEnumerable GetRecordRectangles() - { - if (!HasAttribute("rects")) - yield break; - - // There is a lingering issue in Graphviz where the x coordinates of the record rectangles may be off. - // As a workaround we consult the x coordinates, and attempt to snap onto those. - // https://github.com/Rubjerg/Graphviz.NetWrapper/issues/30 - var validXCoords = GetDrawing().OfType() - .SelectMany(p => p.Value.Points).Select(p => p.X).ToList(); - - foreach (string rectStr in GetAttribute("rects").Split(' ')) - { - var rect = ParseRect(rectStr); - - var x1 = rect.X; - var x2 = rect.X + rect.Width; - var fixedX1 = (float)FindClosest(validXCoords, x1); - var fixedX2 = (float)FindClosest(validXCoords, x2); - var fixedRect = new RectangleF( - new PointF(fixedX1, rect.Y), - new SizeF(fixedX2 - rect.X, rect.Height)); - yield return fixedRect; - } - } + // There is a lingering issue in Graphviz where the x coordinates of the record rectangles may be off. + // As a workaround we consult the x coordinates, and attempt to snap onto those. + // https://github.com/Rubjerg/Graphviz.NetWrapper/issues/30 + var validXCoords = GetDrawing().OfType() + .SelectMany(p => p.Value.Points).Select(p => p.X).ToList(); - /// - /// Return the value that is closest to the given target value. - /// Return target if the sequence if empty. - /// - private static double FindClosest(IEnumerable self, double target) + foreach (string rectStr in GetAttribute("rects").Split(' ')) { - if (self.Any()) - return self.OrderBy(x => Math.Abs(x - target)).First(); - return target; + var rect = ParseRect(rectStr); + + var x1 = rect.X; + var x2 = rect.X + rect.Width; + var fixedX1 = (float)FindClosest(validXCoords, x1); + var fixedX2 = (float)FindClosest(validXCoords, x2); + var fixedRect = new RectangleF( + new PointF(fixedX1, rect.Y), + new SizeF(fixedX2 - rect.X, rect.Height)); + yield return fixedRect; } + } - private RectangleF ParseRect(string rect) - { - string[] points = rect.Split(','); - float leftX = float.Parse(points[0], NumberStyles.Any, CultureInfo.InvariantCulture); - float upperY = float.Parse(points[1], NumberStyles.Any, CultureInfo.InvariantCulture); - float rightX = float.Parse(points[2], NumberStyles.Any, CultureInfo.InvariantCulture); - float lowerY = float.Parse(points[3], NumberStyles.Any, CultureInfo.InvariantCulture); - return new RectangleF(leftX, upperY, rightX - leftX, lowerY - upperY); - } + /// + /// Return the value that is closest to the given target value. + /// Return target if the sequence if empty. + /// + private static double FindClosest(IEnumerable self, double target) + { + if (self.Any()) + return self.OrderBy(x => Math.Abs(x - target)).First(); + return target; + } - public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); - public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); + private RectangleF ParseRect(string rect) + { + string[] points = rect.Split(','); + float leftX = float.Parse(points[0], NumberStyles.Any, CultureInfo.InvariantCulture); + float upperY = float.Parse(points[1], NumberStyles.Any, CultureInfo.InvariantCulture); + float rightX = float.Parse(points[2], NumberStyles.Any, CultureInfo.InvariantCulture); + float lowerY = float.Parse(points[3], NumberStyles.Any, CultureInfo.InvariantCulture); + return new RectangleF(leftX, upperY, rightX - leftX, lowerY - upperY); + } - #endregion + public IReadOnlyList GetDrawing() => GetXDotValue(this, "_draw_"); + public IReadOnlyList GetLabelDrawing() => GetXDotValue(this, "_ldraw_"); - [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] - public GraphvizLabel GetLabel() - { - IntPtr labelptr = NodeLabel(_ptr); - if (labelptr == IntPtr.Zero) - return null; - return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered, new PointF(0, 0)); - } + #endregion + + [Obsolete("This method is only available after ComputeLayout(), and may crash otherwise. It is obsoleted by GetLabelDrawing(). Refer to tutorial.")] + public GraphvizLabel GetLabel() + { + IntPtr labelptr = NodeLabel(_ptr); + if (labelptr == IntPtr.Zero) + return null; + return new GraphvizLabel(labelptr, BoundingBoxCoords.Centered, new PointF(0, 0)); } } diff --git a/Rubjerg.Graphviz/RootGraph.cs b/Rubjerg.Graphviz/RootGraph.cs index 55f570a..807db73 100644 --- a/Rubjerg.Graphviz/RootGraph.cs +++ b/Rubjerg.Graphviz/RootGraph.cs @@ -3,97 +3,96 @@ using System.Linq; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public enum GraphType +{ + Directed = 0, + StrictDirected = 1, + Undirected = 2, + StrictUndirected = 3 +} + +/// +/// Wraps a cgraph root graph. +/// NB: If there is no .net wrapper left that points to any part of a root graph, the root graph is destroyed. +/// +public class RootGraph : Graph { - public enum GraphType + private long _added_pressure = 0; + protected RootGraph(IntPtr ptr) : base(ptr, null) { } + ~RootGraph() { - Directed = 0, - StrictDirected = 1, - Undirected = 2, - StrictUndirected = 3 + if (_added_pressure > 0) + GC.RemoveMemoryPressure(_added_pressure); + _ = Agclose(_ptr); } /// - /// Wraps a cgraph root graph. - /// NB: If there is no .net wrapper left that points to any part of a root graph, the root graph is destroyed. + /// Notify the garbage collector of the approximate allocated unmanaged memory used by this graph. + /// Because it is too much of a hassle to track the exact amount of unmanaged bytes allocated, + /// we use a rough estimate that is hopefully large enough in most cases to prevent OutOfMemory exceptions, + /// but hopefully not too large to completely kill GC performance. + /// This method ignores memory used by attributes. /// - public class RootGraph : Graph + public void UpdateMemoryPressure() { - private long _added_pressure = 0; - protected RootGraph(IntPtr ptr) : base(ptr, null) { } - ~RootGraph() - { - if (_added_pressure > 0) - GC.RemoveMemoryPressure(_added_pressure); - _ = Agclose(_ptr); - } - - /// - /// Notify the garbage collector of the approximate allocated unmanaged memory used by this graph. - /// Because it is too much of a hassle to track the exact amount of unmanaged bytes allocated, - /// we use a rough estimate that is hopefully large enough in most cases to prevent OutOfMemory exceptions, - /// but hopefully not too large to completely kill GC performance. - /// This method ignores memory used by attributes. - /// - public void UpdateMemoryPressure() - { - if (_added_pressure > 0) - GC.RemoveMemoryPressure(_added_pressure); + if (_added_pressure > 0) + GC.RemoveMemoryPressure(_added_pressure); - // Up memory pressure proportional to the amount of unmanaged memory in use. - long unmanaged_bytes_estimate = Nodes().Count() * 104 + Edges().Count() * 64; - if (unmanaged_bytes_estimate > 0) - GC.AddMemoryPressure(unmanaged_bytes_estimate); - _added_pressure = unmanaged_bytes_estimate; - } + // Up memory pressure proportional to the amount of unmanaged memory in use. + long unmanaged_bytes_estimate = Nodes().Count() * 104 + Edges().Count() * 64; + if (unmanaged_bytes_estimate > 0) + GC.AddMemoryPressure(unmanaged_bytes_estimate); + _added_pressure = unmanaged_bytes_estimate; + } - /// - /// Create a new graph. - /// - /// - /// The name is not interpreted by Graphviz, - /// except it is recorded and preserved when the graph is written as a file - /// - public static RootGraph CreateNew(GraphType graphtype, string name = null) - { - name = NameString(name); - var ptr = Rjagopen(name, (int)graphtype); - return new RootGraph(ptr); - } + /// + /// Create a new graph. + /// + /// + /// The name is not interpreted by Graphviz, + /// except it is recorded and preserved when the graph is written as a file + /// + public static RootGraph CreateNew(GraphType graphtype, string name = null) + { + name = NameString(name); + var ptr = Rjagopen(name, (int)graphtype); + return new RootGraph(ptr); + } - public static RootGraph FromDotFile(string filename) - { - string input; - using (var sr = new StreamReader(filename)) - input = sr.ReadToEnd(); + public static RootGraph FromDotFile(string filename) + { + string input; + using (var sr = new StreamReader(filename)) + input = sr.ReadToEnd(); - return FromDotString(input); - } + return FromDotString(input); + } - protected static T FromDotString(string graph, Func constructor) - where T : RootGraph + protected static T FromDotString(string graph, Func constructor) + where T : RootGraph + { + // Just to be safe, make sure the input has unix line endings. Graphviz does not properly support + // windows line endings passed to stdin when it comes to attribute line continuations. + var normalizedDotString = graph.Replace("\r\n", "\n"); + IntPtr ptr = Rjagmemread(normalizedDotString); + if (ptr == IntPtr.Zero) { - // Just to be safe, make sure the input has unix line endings. Graphviz does not properly support - // windows line endings passed to stdin when it comes to attribute line continuations. - var normalizedDotString = graph.Replace("\r\n", "\n"); - IntPtr ptr = Rjagmemread(normalizedDotString); - if (ptr == IntPtr.Zero) - { - throw new InvalidOperationException("Could not create graph"); - } - var result = constructor(ptr); - result.UpdateMemoryPressure(); - return result; + throw new InvalidOperationException("Could not create graph"); } + var result = constructor(ptr); + result.UpdateMemoryPressure(); + return result; + } - public static RootGraph FromDotString(string graph) - { - return FromDotString(graph, ptr => new RootGraph(ptr)); - } + public static RootGraph FromDotString(string graph) + { + return FromDotString(graph, ptr => new RootGraph(ptr)); + } - public void ConvertToUndirectedGraph() - { - ConvertToUndirected(_ptr); - } + public void ConvertToUndirectedGraph() + { + ConvertToUndirected(_ptr); } } diff --git a/Rubjerg.Graphviz/SubGraph.cs b/Rubjerg.Graphviz/SubGraph.cs index 0fa05f7..b7af1c4 100644 --- a/Rubjerg.Graphviz/SubGraph.cs +++ b/Rubjerg.Graphviz/SubGraph.cs @@ -2,67 +2,66 @@ using System.Collections.Generic; using static Rubjerg.Graphviz.ForeignFunctionInterface; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +public class SubGraph : Graph { - public class SubGraph : Graph - { - /// - /// rootgraph must not be null - /// - internal SubGraph(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } + /// + /// rootgraph must not be null + /// + internal SubGraph(IntPtr ptr, RootGraph rootgraph) : base(ptr, rootgraph) { } - internal static SubGraph Get(Graph parent, string name = null) - { - name = NameString(name); - IntPtr ptr = Agsubg(parent._ptr, name, 0); - if (ptr == IntPtr.Zero) - return null; - return new SubGraph(ptr, parent.MyRootGraph); + internal static SubGraph Get(Graph parent, string name = null) + { + name = NameString(name); + IntPtr ptr = Agsubg(parent._ptr, name, 0); + if (ptr == IntPtr.Zero) + return null; + return new SubGraph(ptr, parent.MyRootGraph); - } + } - internal static SubGraph GetOrCreate(Graph parent, string name = null) - { - name = NameString(name); - IntPtr ptr = Agsubg(parent._ptr, name, 1); - return new SubGraph(ptr, parent.MyRootGraph); - } + internal static SubGraph GetOrCreate(Graph parent, string name = null) + { + name = NameString(name); + IntPtr ptr = Agsubg(parent._ptr, name, 1); + return new SubGraph(ptr, parent.MyRootGraph); + } - public void AddExisting(Node node) - { - _ = Agsubnode(_ptr, node._ptr, 1); - } + public void AddExisting(Node node) + { + _ = Agsubnode(_ptr, node._ptr, 1); + } - public void AddExisting(Edge edge) - { - _ = Agsubedge(_ptr, edge._ptr, 1); - } + public void AddExisting(Edge edge) + { + _ = Agsubedge(_ptr, edge._ptr, 1); + } - /// - /// FIXME: use an actual subg equivalent to agsubedge and agsubnode - /// https://github.com/ellson/graphviz/issues/1206 - /// This might cause a new subgraph creation. - /// - public void AddExisting(SubGraph subgraph) - { - _ = Agsubg(_ptr, subgraph.GetName(), 1); - } + /// + /// FIXME: use an actual subg equivalent to agsubedge and agsubnode + /// https://github.com/ellson/graphviz/issues/1206 + /// This might cause a new subgraph creation. + /// + public void AddExisting(SubGraph subgraph) + { + _ = Agsubg(_ptr, subgraph.GetName(), 1); + } - public void AddExisting(IEnumerable nodes) - { - foreach (var node in nodes) - AddExisting(node); - } + public void AddExisting(IEnumerable nodes) + { + foreach (var node in nodes) + AddExisting(node); + } - public void AddExisting(IEnumerable edges) - { - foreach (var edge in edges) - AddExisting(edge); - } + public void AddExisting(IEnumerable edges) + { + foreach (var edge in edges) + AddExisting(edge); + } - public void Delete() - { - _ = Agclose(_ptr); - } + public void Delete() + { + _ = Agclose(_ptr); } } diff --git a/Rubjerg.Graphviz/XDot.cs b/Rubjerg.Graphviz/XDot.cs index fb29500..7a878bc 100644 --- a/Rubjerg.Graphviz/XDot.cs +++ b/Rubjerg.Graphviz/XDot.cs @@ -1,242 +1,241 @@ using System; using System.Drawing; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +// See https://graphviz.org/docs/outputs/canon/#xdot + +public record struct XDotColorStop +{ + public float Frac { get; init; } + public string Color { get; init; } +} + +public record struct XDotLinearGrad +{ + public double X0 { get; init; } + public double Y0 { get; init; } + public double X1 { get; init; } + public double Y1 { get; init; } + public int NStops { get; init; } + public XDotColorStop[] Stops { get; init; } +} + +public record struct XDotRadialGrad { - // See https://graphviz.org/docs/outputs/canon/#xdot + public double X0 { get; init; } + public double Y0 { get; init; } + public double R0 { get; init; } + public double X1 { get; init; } + public double Y1 { get; init; } + public double R1 { get; init; } + public int NStops { get; init; } + public XDotColorStop[] Stops { get; init; } +} - public record struct XDotColorStop +public abstract record class XDotGradColor +{ + private XDotGradColor() { } + public sealed record class Uniform : XDotGradColor { - public float Frac { get; init; } public string Color { get; init; } } - - public record struct XDotLinearGrad + public sealed record class LinearGradient : XDotGradColor { - public double X0 { get; init; } - public double Y0 { get; init; } - public double X1 { get; init; } - public double Y1 { get; init; } - public int NStops { get; init; } - public XDotColorStop[] Stops { get; init; } + public XDotLinearGrad LinearGrad { get; init; } } - - public record struct XDotRadialGrad + public sealed record class RadialGradient : XDotGradColor { - public double X0 { get; init; } - public double Y0 { get; init; } - public double R0 { get; init; } - public double X1 { get; init; } - public double Y1 { get; init; } - public double R1 { get; init; } - public int NStops { get; init; } - public XDotColorStop[] Stops { get; init; } + public XDotRadialGrad RadialGrad { get; init; } } +} - public abstract record class XDotGradColor - { - private XDotGradColor() { } - public sealed record class Uniform : XDotGradColor - { - public string Color { get; init; } - } - public sealed record class LinearGradient : XDotGradColor - { - public XDotLinearGrad LinearGrad { get; init; } - } - public sealed record class RadialGradient : XDotGradColor - { - public XDotRadialGrad RadialGrad { get; init; } - } - } +public record struct XDotPoint +{ + public double X { get; init; } + public double Y { get; init; } + public double Z { get; init; } - public record struct XDotPoint - { - public double X { get; init; } - public double Y { get; init; } - public double Z { get; init; } + public PointF ToPointF() => new PointF((float)X, (float)Y); +} - public PointF ToPointF() => new PointF((float)X, (float)Y); - } +public record struct XDotRect +{ + public double X { get; init; } + public double Y { get; init; } + public double Width { get; init; } + public double Height { get; init; } - public record struct XDotRect - { - public double X { get; init; } - public double Y { get; init; } - public double Width { get; init; } - public double Height { get; init; } + public RectangleF ToRectangleF() => new RectangleF((float)X, (float)Y, (float)Width, (float)Height); +} - public RectangleF ToRectangleF() => new RectangleF((float)X, (float)Y, (float)Width, (float)Height); - } +public record struct XDotPolyline +{ + public int Count { get; init; } + public XDotPoint[] Points { get; init; } +} - public record struct XDotPolyline - { - public int Count { get; init; } - public XDotPoint[] Points { get; init; } - } +public enum XDotAlign +{ + Left, + Center, + Right +} + +/// +/// Represents a line of text to be drawn. +/// Labels with multiple lines will be represented by multiple instances. +/// +public record struct XDotText +{ + /// + /// The X coordinate of the anchor point of the text. + /// + public double X { get; init; } + /// + /// The Y coordinate of the baseline of the text. + /// + public double Y { get; init; } + /// + /// How the text should be aligned, relative to the given anchor point. + /// + public XDotAlign Align { get; init; } + public double Width { get; init; } + public string Text { get; init; } - public enum XDotAlign + /// + /// Compute the bounding box of this text element given the necessary font information. + /// + /// Font used to draw the text + /// Optional property of the font, to more accurately predict the bounding box. + public RectangleF TextBoundingBox(XDotFont font, float? distanceBetweenBaselineAndDescender = null) { - Left, - Center, - Right + var size = Size(font); + var descenderY = Y - (distanceBetweenBaselineAndDescender ?? font.Size / 5); + var leftX = Align switch + { + XDotAlign.Left => X, + XDotAlign.Center => X + size.Width / 2, + XDotAlign.Right => X + size.Width, + _ => throw new InvalidOperationException() + }; + var bottomLeft = new PointF((float)leftX, (float)descenderY); + return new RectangleF(bottomLeft, size); } /// - /// Represents a line of text to be drawn. - /// Labels with multiple lines will be represented by multiple instances. + /// The anchor point of the text. + /// The Y coordinate points to the baseline of the text. + /// The X coordinate points to the horizontal anchor of the text. /// - public record struct XDotText - { - /// - /// The X coordinate of the anchor point of the text. - /// - public double X { get; init; } - /// - /// The Y coordinate of the baseline of the text. - /// - public double Y { get; init; } - /// - /// How the text should be aligned, relative to the given anchor point. - /// - public XDotAlign Align { get; init; } - public double Width { get; init; } - public string Text { get; init; } - - /// - /// Compute the bounding box of this text element given the necessary font information. - /// - /// Font used to draw the text - /// Optional property of the font, to more accurately predict the bounding box. - public RectangleF TextBoundingBox(XDotFont font, float? distanceBetweenBaselineAndDescender = null) - { - var size = Size(font); - var descenderY = Y - (distanceBetweenBaselineAndDescender ?? font.Size / 5); - var leftX = Align switch - { - XDotAlign.Left => X, - XDotAlign.Center => X + size.Width / 2, - XDotAlign.Right => X + size.Width, - _ => throw new InvalidOperationException() - }; - var bottomLeft = new PointF((float)leftX, (float)descenderY); - return new RectangleF(bottomLeft, size); - } - - /// - /// The anchor point of the text. - /// The Y coordinate points to the baseline of the text. - /// The X coordinate points to the horizontal anchor of the text. - /// - public PointF Anchor() => new PointF((float)X, (float)Y); - - /// - /// The width represents the estimated width of the text by GraphViz. - /// The height represents the font size, which is usually the distance between the ascender and the descender - /// of the font. - /// - public SizeF Size(XDotFont font) => new SizeF((float)Width, (float)font.Size); - } - - public record struct XDotImage - { - public XDotRect Pos { get; init; } - public string Name { get; init; } - } - - public record struct XDotFont - { - /// - /// Size in points - /// - public double Size { get; init; } - public string Name { get; init; } - public static XDotFont Default => new() { Size = 14, Name = "Times-Roman" }; - } - - [Flags] - public enum XDotFontChar - { - None = 0, - Bold = 1, - Italic = 2, - Underline = 4, - Superscript = 8, - Subscript = 16, - StrikeThrough = 32, - Overline = 64, - } + public PointF Anchor() => new PointF((float)X, (float)Y); /// - /// See https://graphviz.org/docs/outputs/canon/#xdot for semantics + /// The width represents the estimated width of the text by GraphViz. + /// The height represents the font size, which is usually the distance between the ascender and the descender + /// of the font. /// - public abstract record class XDotOp - { - private XDotOp() { } + public SizeF Size(XDotFont font) => new SizeF((float)Width, (float)font.Size); +} - public sealed record class FilledEllipse : XDotOp - { - public XDotRect Value { get; init; } - } - public sealed record class UnfilledEllipse : XDotOp - { - public XDotRect Value { get; init; } - } - public sealed record class FilledPolygon : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class UnfilledPolygon : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class PolyLine : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class FilledBezier : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class UnfilledBezier : XDotOp - { - public XDotPolyline Value { get; init; } - } - public sealed record class Text : XDotOp - { - public XDotText Value { get; init; } - } - public sealed record class Image : XDotOp - { - public XDotImage Value { get; init; } - } - public sealed record class FillColor : XDotOp - { - public string Value { get; init; } - } - public sealed record class PenColor : XDotOp - { - public string Value { get; init; } - } - public sealed record class GradFillColor : XDotOp - { - public XDotGradColor Value { get; init; } - } - public sealed record class GradPenColor : XDotOp - { - public XDotGradColor Value { get; init; } - } - public sealed record class Font : XDotOp - { - public XDotFont Value { get; init; } - } - public sealed record class Style : XDotOp - { - public string Value { get; init; } - } - public sealed record class FontChar : XDotOp - { - public XDotFontChar Value { get; init; } - } +public record struct XDotImage +{ + public XDotRect Pos { get; init; } + public string Name { get; init; } +} + +public record struct XDotFont +{ + /// + /// Size in points + /// + public double Size { get; init; } + public string Name { get; init; } + public static XDotFont Default => new() { Size = 14, Name = "Times-Roman" }; +} + +[Flags] +public enum XDotFontChar +{ + None = 0, + Bold = 1, + Italic = 2, + Underline = 4, + Superscript = 8, + Subscript = 16, + StrikeThrough = 32, + Overline = 64, +} + +/// +/// See https://graphviz.org/docs/outputs/canon/#xdot for semantics +/// +public abstract record class XDotOp +{ + private XDotOp() { } + + public sealed record class FilledEllipse : XDotOp + { + public XDotRect Value { get; init; } + } + public sealed record class UnfilledEllipse : XDotOp + { + public XDotRect Value { get; init; } + } + public sealed record class FilledPolygon : XDotOp + { + public XDotPolyline Value { get; init; } + } + public sealed record class UnfilledPolygon : XDotOp + { + public XDotPolyline Value { get; init; } + } + public sealed record class PolyLine : XDotOp + { + public XDotPolyline Value { get; init; } + } + public sealed record class FilledBezier : XDotOp + { + public XDotPolyline Value { get; init; } + } + public sealed record class UnfilledBezier : XDotOp + { + public XDotPolyline Value { get; init; } + } + public sealed record class Text : XDotOp + { + public XDotText Value { get; init; } + } + public sealed record class Image : XDotOp + { + public XDotImage Value { get; init; } + } + public sealed record class FillColor : XDotOp + { + public string Value { get; init; } + } + public sealed record class PenColor : XDotOp + { + public string Value { get; init; } + } + public sealed record class GradFillColor : XDotOp + { + public XDotGradColor Value { get; init; } + } + public sealed record class GradPenColor : XDotOp + { + public XDotGradColor Value { get; init; } + } + public sealed record class Font : XDotOp + { + public XDotFont Value { get; init; } + } + public sealed record class Style : XDotOp + { + public string Value { get; init; } + } + public sealed record class FontChar : XDotOp + { + public XDotFontChar Value { get; init; } } } diff --git a/Rubjerg.Graphviz/XDotFFI.cs b/Rubjerg.Graphviz/XDotFFI.cs index 074e69f..65cf459 100644 --- a/Rubjerg.Graphviz/XDotFFI.cs +++ b/Rubjerg.Graphviz/XDotFFI.cs @@ -1,205 +1,203 @@ using System; using System.Runtime.InteropServices; -namespace Rubjerg.Graphviz -{ - /// - /// See https://graphviz.org/docs/outputs/canon/#xdot - /// - internal static class XDotFFI - { - [DllImport("xdot.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr parseXDot([MarshalAs(UnmanagedType.LPStr)] string xdotString); - [DllImport("xdot.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern void freeXDot(IntPtr xdotptr); +namespace Rubjerg.Graphviz; - // Accessors for xdot - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern UIntPtr get_cnt(IntPtr xdot); +/// +/// See https://graphviz.org/docs/outputs/canon/#xdot +/// +internal static class XDotFFI +{ + [DllImport("xdot.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr parseXDot([MarshalAs(UnmanagedType.LPStr)] string xdotString); + [DllImport("xdot.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern void freeXDot(IntPtr xdotptr); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_ops(IntPtr xdot); + // Accessors for xdot + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern UIntPtr get_cnt(IntPtr xdot); - // Accessors for xdot_image - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_name_image(IntPtr img); - public static string GetNameImage(IntPtr img) => Marshal.PtrToStringAnsi(get_name_image(img)); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_ops(IntPtr xdot); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_pos(IntPtr img); + // Accessors for xdot_image + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_name_image(IntPtr img); + public static string GetNameImage(IntPtr img) => Marshal.PtrToStringAnsi(get_name_image(img)); - // Accessors for xdot_font - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_size(IntPtr font); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_pos(IntPtr img); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_name_font(IntPtr font); - public static string GetNameFont(IntPtr img) => Marshal.PtrToStringAnsi(get_name_font(img)); + // Accessors for xdot_font + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_size(IntPtr font); - // Accessors for xdot_op - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern XDotKind get_kind(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_name_font(IntPtr font); + public static string GetNameFont(IntPtr img) => Marshal.PtrToStringAnsi(get_name_font(img)); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_ellipse(IntPtr op); + // Accessors for xdot_op + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern XDotKind get_kind(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_polygon(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_ellipse(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_polyline(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_polygon(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_bezier(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_polyline(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_text(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_bezier(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_image(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_text(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_color(IntPtr op); - public static string GetColor(IntPtr op) => Marshal.PtrToStringAnsi(get_color(op)); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_image(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_grad_color(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_color(IntPtr op); + public static string GetColor(IntPtr op) => Marshal.PtrToStringAnsi(get_color(op)); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_font(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_grad_color(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_style(IntPtr op); - public static string GetStyle(IntPtr op) => Marshal.PtrToStringAnsi(get_style(op)); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_font(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern uint get_fontchar(IntPtr op); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_style(IntPtr op); + public static string GetStyle(IntPtr op) => Marshal.PtrToStringAnsi(get_style(op)); - // Accessors for xdot_color - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern XDotGradType get_type(IntPtr clr); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern uint get_fontchar(IntPtr op); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_clr(IntPtr clr); - public static string GetClr(IntPtr clr) => Marshal.PtrToStringAnsi(get_clr(clr)); + // Accessors for xdot_color + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern XDotGradType get_type(IntPtr clr); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_ling(IntPtr clr); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_clr(IntPtr clr); + public static string GetClr(IntPtr clr) => Marshal.PtrToStringAnsi(get_clr(clr)); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_ring(IntPtr clr); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_ling(IntPtr clr); - // Accessors for xdot_text - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x_text(IntPtr txt); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_ring(IntPtr clr); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y_text(IntPtr txt); + // Accessors for xdot_text + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x_text(IntPtr txt); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern XDotAlign get_align(IntPtr txt); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y_text(IntPtr txt); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_width(IntPtr txt); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern XDotAlign get_align(IntPtr txt); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_text_str(IntPtr txt); - public static string GetTextStr(IntPtr txt) => Marshal.PtrToStringAnsi(get_text_str(txt)); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_width(IntPtr txt); - // Accessors for xdot_linear_grad - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x0_ling(IntPtr ling); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_text_str(IntPtr txt); + public static string GetTextStr(IntPtr txt) => Marshal.PtrToStringAnsi(get_text_str(txt)); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y0_ling(IntPtr ling); + // Accessors for xdot_linear_grad + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x0_ling(IntPtr ling); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x1_ling(IntPtr ling); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y0_ling(IntPtr ling); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y1_ling(IntPtr ling); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x1_ling(IntPtr ling); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_n_stops_ling(IntPtr ling); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y1_ling(IntPtr ling); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_stops_ling(IntPtr ling); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int get_n_stops_ling(IntPtr ling); - // Accessors for xdot_radial_grad - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x0_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_stops_ling(IntPtr ling); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y0_ring(IntPtr ring); + // Accessors for xdot_radial_grad + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x0_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_r0_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y0_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x1_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_r0_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y1_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x1_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_r1_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y1_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern int get_n_stops_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_r1_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_stops_ring(IntPtr ring); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern int get_n_stops_ring(IntPtr ring); - // Accessors for xdot_color_stop - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern float get_frac(IntPtr stop); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_stops_ring(IntPtr ring); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - private static extern IntPtr get_color_stop(IntPtr stop); - public static string GetColorStop(IntPtr stop) => Marshal.PtrToStringAnsi(get_color_stop(stop)); + // Accessors for xdot_color_stop + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern float get_frac(IntPtr stop); - // Accessors for xdot_polyline - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern UIntPtr get_cnt_polyline(IntPtr polyline); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + private static extern IntPtr get_color_stop(IntPtr stop); + public static string GetColorStop(IntPtr stop) => Marshal.PtrToStringAnsi(get_color_stop(stop)); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_pts_polyline(IntPtr polyline); + // Accessors for xdot_polyline + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern UIntPtr get_cnt_polyline(IntPtr polyline); - // Accessors for xdot_point - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x_point(IntPtr point); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_pts_polyline(IntPtr polyline); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y_point(IntPtr point); + // Accessors for xdot_point + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x_point(IntPtr point); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_z_point(IntPtr point); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y_point(IntPtr point); - // Accessors for xdot_rect - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_x_rect(IntPtr rect); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_z_point(IntPtr point); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_y_rect(IntPtr rect); + // Accessors for xdot_rect + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_x_rect(IntPtr rect); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_w_rect(IntPtr rect); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_y_rect(IntPtr rect); - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern double get_h_rect(IntPtr rect); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_w_rect(IntPtr rect); - // Index function for xdot_color_stop array - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_color_stop_at_index(IntPtr stops, int index); + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern double get_h_rect(IntPtr rect); - // Index function for xdot_op array - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_op_at_index(IntPtr ops, int index); + // Index function for xdot_color_stop array + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_color_stop_at_index(IntPtr stops, int index); - // Index function for xdot_pt array - [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr get_pt_at_index(IntPtr pts, int index); + // Index function for xdot_op array + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_op_at_index(IntPtr ops, int index); - } + // Index function for xdot_pt array + [DllImport("GraphvizWrapper.dll", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr get_pt_at_index(IntPtr pts, int index); } diff --git a/Rubjerg.Graphviz/XDotParser.cs b/Rubjerg.Graphviz/XDotParser.cs index 77dc4bf..67cf39c 100644 --- a/Rubjerg.Graphviz/XDotParser.cs +++ b/Rubjerg.Graphviz/XDotParser.cs @@ -2,350 +2,349 @@ using System.Collections.Generic; using System.Linq; -namespace Rubjerg.Graphviz +namespace Rubjerg.Graphviz; + +// These internal types are only used for marshaling +// We replace them with more idiomatic types +internal enum XDotGradType { - // These internal types are only used for marshaling - // We replace them with more idiomatic types - internal enum XDotGradType - { - None, - Linear, - Radial - } + None, + Linear, + Radial +} - internal enum XDotKind - { - FilledEllipse, UnfilledEllipse, - FilledPolygon, UnfilledPolygon, - FilledBezier, UnfilledBezier, - Polyline, Text, - FillColor, PenColor, Font, Style, Image, - GradFillColor, GradPenColor, - FontChar - } +internal enum XDotKind +{ + FilledEllipse, UnfilledEllipse, + FilledPolygon, UnfilledPolygon, + FilledBezier, UnfilledBezier, + Polyline, Text, + FillColor, PenColor, Font, Style, Image, + GradFillColor, GradPenColor, + FontChar +} - internal struct XDot - { - public int Count { get; set; } // Number of xdot ops - public XDotOp[] Ops { get; set; } // xdot operations - } +internal struct XDot +{ + public int Count { get; set; } // Number of xdot ops + public XDotOp[] Ops { get; set; } // xdot operations +} - internal static class XDotParser +internal static class XDotParser +{ + public static List ParseXDot(string xdotString) { - public static List ParseXDot(string xdotString) + IntPtr xdot = XDotFFI.parseXDot(xdotString); + try { - IntPtr xdot = XDotFFI.parseXDot(xdotString); - try - { - return TranslateXDot(xdot); - } - finally - { - if (xdot != IntPtr.Zero) - { - XDotFFI.freeXDot(xdot); - } - } + return TranslateXDot(xdot); } - - internal static List TranslateXDot(IntPtr xdotPtr) + finally { - if (xdotPtr == IntPtr.Zero) - throw new ArgumentNullException(nameof(xdotPtr)); - - XDot xdot = new XDot - { - Count = (int)XDotFFI.get_cnt(xdotPtr) - }; - - // Translate the array of XDotOps - int count = xdot.Count; - xdot.Ops = new XDotOp[count]; - var opsPtr = XDotFFI.get_ops(xdotPtr); - for (int i = 0; i < count; ++i) + if (xdot != IntPtr.Zero) { - IntPtr xdotOpPtr = XDotFFI.get_op_at_index(opsPtr, i); - xdot.Ops[i] = TranslateXDotOp(xdotOpPtr); + XDotFFI.freeXDot(xdot); } - - return xdot.Ops.ToList(); } + } - private static XDotOp TranslateXDotOp(IntPtr xdotOpPtr) - { - if (xdotOpPtr == IntPtr.Zero) - throw new ArgumentNullException(nameof(xdotOpPtr)); + internal static List TranslateXDot(IntPtr xdotPtr) + { + if (xdotPtr == IntPtr.Zero) + throw new ArgumentNullException(nameof(xdotPtr)); - var kind = XDotFFI.get_kind(xdotOpPtr); - switch (kind) - { - case XDotKind.FilledEllipse: - return new XDotOp.FilledEllipse() - { - Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) - }; - case XDotKind.UnfilledEllipse: - return new XDotOp.UnfilledEllipse() - { - Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) - }; - case XDotKind.FilledPolygon: - return new XDotOp.FilledPolygon() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.UnfilledPolygon: - return new XDotOp.FilledPolygon() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.FilledBezier: - return new XDotOp.FilledBezier() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.UnfilledBezier: - return new XDotOp.UnfilledBezier() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.Polyline: - return new XDotOp.PolyLine() - { - Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) - }; - case XDotKind.Text: - return new XDotOp.Text() - { - Value = TranslateText(XDotFFI.get_text(xdotOpPtr)) - }; - case XDotKind.FillColor: - return new XDotOp.FillColor() - { - Value = XDotFFI.GetColor(xdotOpPtr) - }; - case XDotKind.PenColor: - return new XDotOp.PenColor() - { - Value = XDotFFI.GetColor(xdotOpPtr) - }; - case XDotKind.GradFillColor: - return new XDotOp.GradFillColor() - { - Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) - }; - case XDotKind.GradPenColor: - return new XDotOp.GradPenColor() - { - Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) - }; - case XDotKind.Font: - return new XDotOp.Font() - { - Value = TranslateFont(XDotFFI.get_font(xdotOpPtr)) - }; - case XDotKind.Style: - return new XDotOp.Style() - { - Value = XDotFFI.GetStyle(xdotOpPtr) - }; - case XDotKind.Image: - return new XDotOp.Image() - { - Value = TranslateImage(XDotFFI.get_image(xdotOpPtr)) - }; - case XDotKind.FontChar: - return new XDotOp.FontChar() - { - Value = TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr)) - }; - default: - throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); - } + XDot xdot = new XDot + { + Count = (int)XDotFFI.get_cnt(xdotPtr) + }; + + // Translate the array of XDotOps + int count = xdot.Count; + xdot.Ops = new XDotOp[count]; + var opsPtr = XDotFFI.get_ops(xdotPtr); + for (int i = 0; i < count; ++i) + { + IntPtr xdotOpPtr = XDotFFI.get_op_at_index(opsPtr, i); + xdot.Ops[i] = TranslateXDotOp(xdotOpPtr); } - private static XDotFontChar TranslateFontChar(uint value) + return xdot.Ops.ToList(); + } + + private static XDotOp TranslateXDotOp(IntPtr xdotOpPtr) + { + if (xdotOpPtr == IntPtr.Zero) + throw new ArgumentNullException(nameof(xdotOpPtr)); + + var kind = XDotFFI.get_kind(xdotOpPtr); + switch (kind) { - return (XDotFontChar)(int)value; + case XDotKind.FilledEllipse: + return new XDotOp.FilledEllipse() + { + Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) + }; + case XDotKind.UnfilledEllipse: + return new XDotOp.UnfilledEllipse() + { + Value = TranslateEllipse(XDotFFI.get_ellipse(xdotOpPtr)) + }; + case XDotKind.FilledPolygon: + return new XDotOp.FilledPolygon() + { + Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + }; + case XDotKind.UnfilledPolygon: + return new XDotOp.FilledPolygon() + { + Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + }; + case XDotKind.FilledBezier: + return new XDotOp.FilledBezier() + { + Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + }; + case XDotKind.UnfilledBezier: + return new XDotOp.UnfilledBezier() + { + Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + }; + case XDotKind.Polyline: + return new XDotOp.PolyLine() + { + Value = TranslatePolyline(XDotFFI.get_polyline(xdotOpPtr)) + }; + case XDotKind.Text: + return new XDotOp.Text() + { + Value = TranslateText(XDotFFI.get_text(xdotOpPtr)) + }; + case XDotKind.FillColor: + return new XDotOp.FillColor() + { + Value = XDotFFI.GetColor(xdotOpPtr) + }; + case XDotKind.PenColor: + return new XDotOp.PenColor() + { + Value = XDotFFI.GetColor(xdotOpPtr) + }; + case XDotKind.GradFillColor: + return new XDotOp.GradFillColor() + { + Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) + }; + case XDotKind.GradPenColor: + return new XDotOp.GradPenColor() + { + Value = TranslateGradColor(XDotFFI.get_grad_color(xdotOpPtr)) + }; + case XDotKind.Font: + return new XDotOp.Font() + { + Value = TranslateFont(XDotFFI.get_font(xdotOpPtr)) + }; + case XDotKind.Style: + return new XDotOp.Style() + { + Value = XDotFFI.GetStyle(xdotOpPtr) + }; + case XDotKind.Image: + return new XDotOp.Image() + { + Value = TranslateImage(XDotFFI.get_image(xdotOpPtr)) + }; + case XDotKind.FontChar: + return new XDotOp.FontChar() + { + Value = TranslateFontChar(XDotFFI.get_fontchar(xdotOpPtr)) + }; + default: + throw new ArgumentException($"Unexpected XDotOp.Kind: {kind}"); } - private static XDotImage TranslateImage(IntPtr imagePtr) + } + + private static XDotFontChar TranslateFontChar(uint value) + { + return (XDotFontChar)(int)value; + } + private static XDotImage TranslateImage(IntPtr imagePtr) + { + XDotImage image = new XDotImage { - XDotImage image = new XDotImage - { - Pos = TranslateRect(XDotFFI.get_pos(imagePtr)), - Name = XDotFFI.GetNameImage(imagePtr) - }; + Pos = TranslateRect(XDotFFI.get_pos(imagePtr)), + Name = XDotFFI.GetNameImage(imagePtr) + }; - return image; - } + return image; + } - private static XDotFont TranslateFont(IntPtr fontPtr) + private static XDotFont TranslateFont(IntPtr fontPtr) + { + XDotFont font = new XDotFont { - XDotFont font = new XDotFont - { - Size = XDotFFI.get_size(fontPtr), - Name = XDotFFI.GetNameFont(fontPtr) - }; + Size = XDotFFI.get_size(fontPtr), + Name = XDotFFI.GetNameFont(fontPtr) + }; - return font; - } + return font; + } - private static XDotRect TranslateEllipse(IntPtr ellipsePtr) + private static XDotRect TranslateEllipse(IntPtr ellipsePtr) + { + XDotRect ellipse = new XDotRect { - XDotRect ellipse = new XDotRect - { - X = XDotFFI.get_x_rect(ellipsePtr), - Y = XDotFFI.get_y_rect(ellipsePtr), - Width = XDotFFI.get_w_rect(ellipsePtr), - Height = XDotFFI.get_h_rect(ellipsePtr) - }; + X = XDotFFI.get_x_rect(ellipsePtr), + Y = XDotFFI.get_y_rect(ellipsePtr), + Width = XDotFFI.get_w_rect(ellipsePtr), + Height = XDotFFI.get_h_rect(ellipsePtr) + }; - return ellipse; - } + return ellipse; + } - private static XDotGradColor TranslateGradColor(IntPtr colorPtr) + private static XDotGradColor TranslateGradColor(IntPtr colorPtr) + { + var type = XDotFFI.get_type(colorPtr); + switch (type) { - var type = XDotFFI.get_type(colorPtr); - switch (type) - { - case XDotGradType.None: - return new XDotGradColor.Uniform() - { - Color = XDotFFI.GetClr(colorPtr) - }; - case XDotGradType.Linear: - return new XDotGradColor.LinearGradient() - { - LinearGrad = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) - }; - case XDotGradType.Radial: - return new XDotGradColor.RadialGradient() - { - RadialGrad = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) - }; - default: - throw new ArgumentException($"Unexpected XDotColor.Type: {type}"); - } + case XDotGradType.None: + return new XDotGradColor.Uniform() + { + Color = XDotFFI.GetClr(colorPtr) + }; + case XDotGradType.Linear: + return new XDotGradColor.LinearGradient() + { + LinearGrad = TranslateLinearGrad(XDotFFI.get_ling(colorPtr)) + }; + case XDotGradType.Radial: + return new XDotGradColor.RadialGradient() + { + RadialGrad = TranslateRadialGrad(XDotFFI.get_ring(colorPtr)) + }; + default: + throw new ArgumentException($"Unexpected XDotColor.Type: {type}"); } + } - private static XDotLinearGrad TranslateLinearGrad(IntPtr lingPtr) + private static XDotLinearGrad TranslateLinearGrad(IntPtr lingPtr) + { + int count = XDotFFI.get_n_stops_ling(lingPtr); + XDotLinearGrad linearGrad = new XDotLinearGrad { - int count = XDotFFI.get_n_stops_ling(lingPtr); - XDotLinearGrad linearGrad = new XDotLinearGrad - { - X0 = XDotFFI.get_x0_ling(lingPtr), - Y0 = XDotFFI.get_y0_ling(lingPtr), - X1 = XDotFFI.get_x1_ling(lingPtr), - Y1 = XDotFFI.get_y1_ling(lingPtr), - NStops = count, - Stops = new XDotColorStop[count] - }; - - // Translate the array of ColorStops - var stopsPtr = XDotFFI.get_stops_ling(lingPtr); - for (int i = 0; i < count; ++i) - { - IntPtr colorStopPtr = XDotFFI.get_color_stop_at_index(stopsPtr, i); - linearGrad.Stops[i] = TranslateColorStop(colorStopPtr); - } - - return linearGrad; + X0 = XDotFFI.get_x0_ling(lingPtr), + Y0 = XDotFFI.get_y0_ling(lingPtr), + X1 = XDotFFI.get_x1_ling(lingPtr), + Y1 = XDotFFI.get_y1_ling(lingPtr), + NStops = count, + Stops = new XDotColorStop[count] + }; + + // Translate the array of ColorStops + var stopsPtr = XDotFFI.get_stops_ling(lingPtr); + for (int i = 0; i < count; ++i) + { + IntPtr colorStopPtr = XDotFFI.get_color_stop_at_index(stopsPtr, i); + linearGrad.Stops[i] = TranslateColorStop(colorStopPtr); } - private static XDotRadialGrad TranslateRadialGrad(IntPtr ringPtr) - { - int count = XDotFFI.get_n_stops_ring(ringPtr); - XDotRadialGrad radialGrad = new XDotRadialGrad - { - X0 = XDotFFI.get_x0_ring(ringPtr), - Y0 = XDotFFI.get_y0_ring(ringPtr), - R0 = XDotFFI.get_r0_ring(ringPtr), - X1 = XDotFFI.get_x1_ring(ringPtr), - Y1 = XDotFFI.get_y1_ring(ringPtr), - R1 = XDotFFI.get_r1_ring(ringPtr), - NStops = count, - Stops = new XDotColorStop[count] - }; - - // Translate the array of ColorStops - var stopsPtr = XDotFFI.get_stops_ring(ringPtr); - for (int i = 0; i < count; ++i) - { - IntPtr colorStopPtr = XDotFFI.get_color_stop_at_index(stopsPtr, i); - radialGrad.Stops[i] = TranslateColorStop(colorStopPtr); - } + return linearGrad; + } - return radialGrad; + private static XDotRadialGrad TranslateRadialGrad(IntPtr ringPtr) + { + int count = XDotFFI.get_n_stops_ring(ringPtr); + XDotRadialGrad radialGrad = new XDotRadialGrad + { + X0 = XDotFFI.get_x0_ring(ringPtr), + Y0 = XDotFFI.get_y0_ring(ringPtr), + R0 = XDotFFI.get_r0_ring(ringPtr), + X1 = XDotFFI.get_x1_ring(ringPtr), + Y1 = XDotFFI.get_y1_ring(ringPtr), + R1 = XDotFFI.get_r1_ring(ringPtr), + NStops = count, + Stops = new XDotColorStop[count] + }; + + // Translate the array of ColorStops + var stopsPtr = XDotFFI.get_stops_ring(ringPtr); + for (int i = 0; i < count; ++i) + { + IntPtr colorStopPtr = XDotFFI.get_color_stop_at_index(stopsPtr, i); + radialGrad.Stops[i] = TranslateColorStop(colorStopPtr); } - private static XDotColorStop TranslateColorStop(IntPtr stopPtr) + return radialGrad; + } + + private static XDotColorStop TranslateColorStop(IntPtr stopPtr) + { + XDotColorStop colorStop = new XDotColorStop { - XDotColorStop colorStop = new XDotColorStop - { - Frac = XDotFFI.get_frac(stopPtr), - Color = XDotFFI.GetColorStop(stopPtr) - }; + Frac = XDotFFI.get_frac(stopPtr), + Color = XDotFFI.GetColorStop(stopPtr) + }; - return colorStop; - } + return colorStop; + } - private static XDotPolyline TranslatePolyline(IntPtr polylinePtr) + private static XDotPolyline TranslatePolyline(IntPtr polylinePtr) + { + int count = (int)XDotFFI.get_cnt_polyline(polylinePtr); + XDotPolyline polyline = new XDotPolyline { - int count = (int)XDotFFI.get_cnt_polyline(polylinePtr); - XDotPolyline polyline = new XDotPolyline - { - Count = count, - Points = new XDotPoint[count] - }; + Count = count, + Points = new XDotPoint[count] + }; - // Translate the array of Points - var pointsPtr = XDotFFI.get_pts_polyline(polylinePtr); - for (int i = 0; i < count; ++i) - { - IntPtr pointPtr = XDotFFI.get_pt_at_index(pointsPtr, i); - polyline.Points[i] = TranslatePoint(pointPtr); - } - - return polyline; + // Translate the array of Points + var pointsPtr = XDotFFI.get_pts_polyline(polylinePtr); + for (int i = 0; i < count; ++i) + { + IntPtr pointPtr = XDotFFI.get_pt_at_index(pointsPtr, i); + polyline.Points[i] = TranslatePoint(pointPtr); } - private static XDotPoint TranslatePoint(IntPtr pointPtr) + return polyline; + } + + private static XDotPoint TranslatePoint(IntPtr pointPtr) + { + XDotPoint point = new XDotPoint { - XDotPoint point = new XDotPoint - { - X = XDotFFI.get_x_point(pointPtr), - Y = XDotFFI.get_y_point(pointPtr), - Z = XDotFFI.get_z_point(pointPtr) - }; + X = XDotFFI.get_x_point(pointPtr), + Y = XDotFFI.get_y_point(pointPtr), + Z = XDotFFI.get_z_point(pointPtr) + }; - return point; - } + return point; + } - private static XDotRect TranslateRect(IntPtr rectPtr) + private static XDotRect TranslateRect(IntPtr rectPtr) + { + XDotRect rect = new XDotRect { - XDotRect rect = new XDotRect - { - X = XDotFFI.get_x_rect(rectPtr), - Y = XDotFFI.get_y_rect(rectPtr), - Width = XDotFFI.get_w_rect(rectPtr), - Height = XDotFFI.get_h_rect(rectPtr) - }; + X = XDotFFI.get_x_rect(rectPtr), + Y = XDotFFI.get_y_rect(rectPtr), + Width = XDotFFI.get_w_rect(rectPtr), + Height = XDotFFI.get_h_rect(rectPtr) + }; - return rect; - } + return rect; + } - private static XDotText TranslateText(IntPtr txtPtr) + private static XDotText TranslateText(IntPtr txtPtr) + { + XDotText text = new XDotText { - XDotText text = new XDotText - { - X = XDotFFI.get_x_text(txtPtr), - Y = XDotFFI.get_y_text(txtPtr), - Align = XDotFFI.get_align(txtPtr), - Width = XDotFFI.get_width(txtPtr), - Text = XDotFFI.GetTextStr(txtPtr) - }; - - return text; - } + X = XDotFFI.get_x_text(txtPtr), + Y = XDotFFI.get_y_text(txtPtr), + Align = XDotFFI.get_align(txtPtr), + Width = XDotFFI.get_width(txtPtr), + Text = XDotFFI.GetTextStr(txtPtr) + }; + + return text; } }