// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Xml; using EpicGames.Core; namespace UnrealBuildTool { /// /// A single "node" in a directed graph /// class GraphNode { public string Label; public Color Color; public float Size; public Dictionary Attributes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); public GraphNode(string Label, float Size = 1.0f) : this(Label, Color.Black, Size) { } public GraphNode(string Label, Color Color, float Size = 1.0f) { this.Label = Label; this.Color = Color; this.Size = Size; } } /// /// Describes an edge in the directed graph /// class GraphEdge { public GraphNode Source; public GraphNode Target; public double Weight = 1.0f; public Color Color = Color.FromArgb(64, 0, 0, 0); public float Thickness = 0.1f; public GraphEdge(GraphNode Source, GraphNode Target) { this.Source = Source; this.Target = Target; } } static class GraphVisualization { /// /// Attribute for a graph node /// class GraphAttribute { /// Gexf ID for this attribute public int Id; /// Name of the attribute public string Name; /// Gexf type name public string TypeName; public GraphAttribute(int Id, string Name, string TypeName) { this.Id = Id; this.Name = Name; this.TypeName = TypeName; } } /// /// Writes a GEXF graph file for the specified graph nodes and edges /// /// The file name to write /// The description to include in the graph file's metadata /// List of all graph nodes. Index order is important and must match with the individual node Id members! /// List of all graph edges. Index order is important and must match with the individual edge Id members! public static void WriteGraphFile(FileReference Filename, string Description, List GraphNodes, List GraphEdges) { CultureInfo OriginalCulture = CultureInfo.CurrentCulture; try { // export graph using invariant culture so "." is used as a decimal separator CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; XmlWriterSettings Settings = new XmlWriterSettings(); Settings.Indent = true; Settings.IndentChars = " "; // Figure out all of the custom attribute types we're dealing with Dictionary AllAttributes = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (GraphNode GraphNode in GraphNodes) { foreach ((string AttributeName, object AttributeValue) in GraphNode.Attributes) { string AttributeType = GetAttributeType(AttributeValue); GraphAttribute? Attribute; if (!AllAttributes.TryGetValue(AttributeName, out Attribute)) { AllAttributes[AttributeName] = new GraphAttribute(AllAttributes.Count, AttributeName, AttributeType); } else if (!Attribute.TypeName.Equals(AttributeType)) { throw new InvalidOperationException("Multiple graph nodes with the same attribute name but different types encountered!"); } } } using (XmlWriter Writer = XmlWriter.Create(Filename.FullName, Settings)) { // NOTE: The GEXF XML format is defined here: http://gexf.net/1.2draft/gexf-12draft-primer.pdf string GEXFNamespace = "http://www.gexf.net/1.2-draft"; string SchemaNamespace = "http://www.w3.org/2001/XMLSchema-instance"; string VizNamespace = "http://www.gexf.net/1.2draft/viz"; Writer.WriteStartElement("gexf", GEXFNamespace); Writer.WriteAttributeString("xmlns", "xsi", null, SchemaNamespace); Writer.WriteAttributeString("schemaLocation", SchemaNamespace, "http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd"); Writer.WriteAttributeString("xmlns", "viz", null, VizNamespace); Writer.WriteAttributeString("version", "1.2"); Writer.WriteStartElement("meta"); { Writer.WriteAttributeString("creator", "UnrealBuildTool"); Writer.WriteAttributeString("description", Description); } Writer.WriteEndElement(); // meta Dictionary NodeToId = new Dictionary(); { Writer.WriteStartElement("graph"); { Writer.WriteAttributeString("mode", "static"); Writer.WriteAttributeString("defaultedgetype", "directed"); if (AllAttributes.Count > 0) { Writer.WriteStartElement("attributes"); { // @todo: Add support for edge attributes, not just node attributes Writer.WriteAttributeString("class", "node"); // Node attributes, not edges! foreach (GraphAttribute Attribute in AllAttributes.Values) { Writer.WriteStartElement("attribute"); { Writer.WriteAttributeString("id", Attribute.Id.ToString()); Writer.WriteAttributeString("title", Attribute.Name); Writer.WriteAttributeString("type", Attribute.TypeName); } Writer.WriteEndElement(); // attribute } // @todo: Add support for attribute type default values } Writer.WriteEndElement(); // attributes } Writer.WriteStartElement("nodes"); { foreach (GraphNode GraphNode in GraphNodes) { Writer.WriteStartElement("node"); { int Id = GetNodeId(GraphNode, NodeToId); Writer.WriteAttributeString("id", Id.ToString()); Writer.WriteAttributeString("label", GraphNode.Label); Writer.WriteStartElement("color", VizNamespace); { Writer.WriteAttributeString("r", GraphNode.Color.R.ToString()); Writer.WriteAttributeString("g", GraphNode.Color.G.ToString()); Writer.WriteAttributeString("b", GraphNode.Color.B.ToString()); Writer.WriteAttributeString("a", (GraphNode.Color.A / 255.0f).ToString()); } Writer.WriteEndElement(); // viz:color Writer.WriteStartElement("size", VizNamespace); { Writer.WriteAttributeString("value", GraphNode.Size.ToString()); } Writer.WriteEndElement(); // viz:size Writer.WriteStartElement("shape", VizNamespace); { // NOTE: Valid shapes are: disc, square, triangle, diamond, image Writer.WriteAttributeString("value", "disc"); } Writer.WriteEndElement(); // viz:shape if (GraphNode.Attributes.Count > 0) { Writer.WriteStartElement("attvalues"); { foreach (KeyValuePair AttributeHashEntry in GraphNode.Attributes) { string AttributeName = AttributeHashEntry.Key; object AttributeValue = AttributeHashEntry.Value; GraphAttribute Attribute = AllAttributes[AttributeName]; Writer.WriteStartElement("attvalue"); { Writer.WriteAttributeString("for", Attribute.Id.ToString()); Writer.WriteAttributeString("value", AttributeValue.ToString()); } Writer.WriteEndElement(); } } Writer.WriteEndElement(); // attvalues } } Writer.WriteEndElement(); // node } } Writer.WriteEndElement(); // nodes Writer.WriteStartElement("edges"); { for (int EdgeId = 0; EdgeId < GraphEdges.Count; EdgeId++) { GraphEdge GraphEdge = GraphEdges[EdgeId]; Writer.WriteStartElement("edge"); { Writer.WriteAttributeString("id", EdgeId.ToString()); Writer.WriteAttributeString("source", GetNodeId(GraphEdge.Source, NodeToId).ToString()); Writer.WriteAttributeString("target", GetNodeId(GraphEdge.Target, NodeToId).ToString()); Writer.WriteAttributeString("weight", GraphEdge.Weight.ToString()); Writer.WriteStartElement("color", VizNamespace); { Writer.WriteAttributeString("r", GraphEdge.Color.R.ToString()); Writer.WriteAttributeString("g", GraphEdge.Color.G.ToString()); Writer.WriteAttributeString("b", GraphEdge.Color.B.ToString()); Writer.WriteAttributeString("a", (GraphEdge.Color.A / 255.0f).ToString()); } Writer.WriteEndElement(); // viz:color Writer.WriteStartElement("thickness", VizNamespace); { Writer.WriteAttributeString("value", GraphEdge.Thickness.ToString()); } Writer.WriteEndElement(); // viz:thickness Writer.WriteStartElement("shape", VizNamespace); { // NOTE: Valid shapes are: solid, dotted, dashed, double Writer.WriteAttributeString("value", "solid"); } Writer.WriteEndElement(); // viz:shape } Writer.WriteEndElement(); // edge } } Writer.WriteEndElement(); // nodes } Writer.WriteEndElement(); // graph } Writer.WriteEndElement(); // gexf Writer.Flush(); } } finally { CultureInfo.CurrentCulture = OriginalCulture; } } private static int GetNodeId(GraphNode Node, Dictionary NodeToId) { int Id; if (!NodeToId.TryGetValue(Node, out Id)) { Id = NodeToId.Count; NodeToId.Add(Node, Id); } return Id; } private static string GetAttributeType(object Value) { string AttributeTypeName; if (Value.GetType() == typeof(int)) { AttributeTypeName = "integer"; } else if (Value.GetType() == typeof(float)) { AttributeTypeName = "float"; } else if (Value.GetType() == typeof(double)) { AttributeTypeName = "double"; } else if (Value.GetType() == typeof(string)) { AttributeTypeName = "string"; } else if (Value.GetType() == typeof(bool)) { AttributeTypeName = "boolean"; } else { throw new InvalidOperationException("Unsupported attribute data type encountered on graph node!"); } return AttributeTypeName; } } }