ImportWorkbookIntoGraph
    (
        Worksheet oSourceWorksheet,
        Range oNonEmptySourceRange,
        Int32 iColumnNumberToUseForVertex1OneBased,
        Int32 iColumnNumberToUseForVertex2OneBased,
        ICollection<Int32> oEdgeColumnNumbersToImportOneBased,
        ICollection<Int32> oVertex1ColumnNumbersToImportOneBased,
        ICollection<Int32> oVertex2ColumnNumbersToImportOneBased,
        Boolean bSourceColumnsHaveHeaders
    )
    {
        Debug.Assert(oSourceWorksheet != null);
        Debug.Assert(oNonEmptySourceRange != null);
        Debug.Assert(iColumnNumberToUseForVertex1OneBased >= 1);
        Debug.Assert(iColumnNumberToUseForVertex2OneBased >= 1);
        Debug.Assert(oEdgeColumnNumbersToImportOneBased != null);
        Debug.Assert(oVertex1ColumnNumbersToImportOneBased != null);
        Debug.Assert(oVertex2ColumnNumbersToImportOneBased != null);
        AssertValid();

        String [] asWorkbookColumnNames = GetWorkbookColumnNames(
            oSourceWorksheet, oNonEmptySourceRange, bSourceColumnsHaveHeaders);

        Int32 iColumns = oNonEmptySourceRange.Columns.Count;

        Int32 iFirstNonEmptyColumnOneBased =
            oNonEmptySourceRange.Columns.Column;

        if (bSourceColumnsHaveHeaders)
        {
            // Skip the header row.

            if (oNonEmptySourceRange.Rows.Count < 2)
            {
                OnInvalidSourceWorkbook(
                    "If the columns in the other workbook have headers, then"
                    + " there must be at least two rows.",

                    oSourceWorksheet, 1, 1
                    );
            }

            ExcelUtil.OffsetRange(ref oNonEmptySourceRange, 1, 0);

            ExcelUtil.ResizeRange(ref oNonEmptySourceRange,
                oNonEmptySourceRange.Rows.Count - 1, iColumns);
        }

        IGraph oGraph = new Graph(GraphDirectedness.Undirected);
        IVertexCollection oVertices = oGraph.Vertices;
        IEdgeCollection oEdges = oGraph.Edges;

        // The key is a vertex name and the value is the corresponding IVertex
        // object.

        Dictionary<String, IVertex> oVertexNameDictionary =
            new Dictionary<String, IVertex>();

        foreach ( Range oSubrange in
            ExcelRangeSplitter.SplitRange(oNonEmptySourceRange, 500) )
        {
            Object [,] oSubrangeValues = ExcelUtil.GetRangeValues(oSubrange);
            Int32 iSubrangeRows = oSubrangeValues.GetUpperBound(0);

            for (Int32 iRowOneBased = 1; iRowOneBased <= iSubrangeRows;
                iRowOneBased++)
            {
                String sVertex1Name, sVertex2Name;

                if (
                    !ExcelUtil.TryGetNonEmptyStringFromCell(oSubrangeValues,
                        iRowOneBased,
                        iColumnNumberToUseForVertex1OneBased -
                            iFirstNonEmptyColumnOneBased + 1,
                        out sVertex1Name)
                    ||
                    !ExcelUtil.TryGetNonEmptyStringFromCell(oSubrangeValues,
                        iRowOneBased,
                        iColumnNumberToUseForVertex2OneBased -
                            iFirstNonEmptyColumnOneBased + 1,
                        out sVertex2Name)
                    )
                {
                    continue;
                }

                IVertex oVertex1 = WorksheetReaderBase.VertexNameToVertex(
                    sVertex1Name, oVertices, oVertexNameDictionary);

                IVertex oVertex2 = WorksheetReaderBase.VertexNameToVertex(
                    sVertex2Name, oVertices, oVertexNameDictionary);

                IEdge oEdge = oEdges.Add(oVertex1, oVertex2);

                // Add the edge and vertex attributes.

                AddAttributeValuesToEdgeOrVertex(asWorkbookColumnNames,
                    oSubrangeValues, iRowOneBased,
                    iFirstNonEmptyColumnOneBased,
                    oEdgeColumnNumbersToImportOneBased, oEdge);

                AddAttributeValuesToEdgeOrVertex(asWorkbookColumnNames,
                    oSubrangeValues, iRowOneBased,
                    iFirstNonEmptyColumnOneBased,
                    oVertex1ColumnNumbersToImportOneBased, oVertex1);

                AddAttributeValuesToEdgeOrVertex(asWorkbookColumnNames,
                    oSubrangeValues, iRowOneBased,
                    iFirstNonEmptyColumnOneBased,
                    oVertex2ColumnNumbersToImportOneBased, oVertex2);
            }
        }

        // Store metadata on the graph indicating the sets of keys that may be
        // present on the graph's edges and vertices.

        oGraph.SetValue(ReservedMetadataKeys.AllEdgeMetadataKeys,

            GetColumnNamesToImport(oNonEmptySourceRange,
                asWorkbookColumnNames, oEdgeColumnNumbersToImportOneBased)
            );

        List<Int32> oVertexColumnNumbersToImportOneBased = new List<Int32>();

        oVertexColumnNumbersToImportOneBased.AddRange(
            oVertex1ColumnNumbersToImportOneBased);

        oVertexColumnNumbersToImportOneBased.AddRange(
            oVertex2ColumnNumbersToImportOneBased);

        oGraph.SetValue( ReservedMetadataKeys.AllVertexMetadataKeys,

            GetColumnNamesToImport(oNonEmptySourceRange, asWorkbookColumnNames,
                oVertexColumnNumbersToImportOneBased)
            );

        return (oGraph);
    }
    TestTryGetGroupLayoutDrawingInfo()
    {
        // Graph has group drawing information.

        const Double GroupRectanglePenWidth = 4.567;

        const IntergroupEdgeStyle IntergroupEdgeStyle =
            IntergroupEdgeStyle.Show;

        IGraph oGraph = new Graph();

        oGraph.SetValue(ReservedMetadataKeys.GroupLayoutDrawingInfo,
            new GroupLayoutDrawingInfo(
                new GroupInfo[] { new GroupInfo(), new GroupInfo() },
                GroupRectanglePenWidth, null
            ) );

        GroupLayoutDrawingInfo oGroupLayoutDrawingInfo;

        Assert.IsTrue( GroupMetadataManager.TryGetGroupLayoutDrawingInfo(
            oGraph, out oGroupLayoutDrawingInfo) );

        Assert.AreEqual(2, oGroupLayoutDrawingInfo.GroupsToDraw.Count);

        Assert.AreEqual(GroupRectanglePenWidth,
            oGroupLayoutDrawingInfo.PenWidth);

        Assert.IsNull(oGroupLayoutDrawingInfo.CombinedIntergroupEdges);
    }
    TestTryTransformGroupRectangles()
    {
        // Graph has group drawing information.

        const Double GroupRectanglePenWidth = 4.567;

        IGraph oGraph = new Graph();

        GroupInfo oGroupInfo1 = new GroupInfo();
        oGroupInfo1.Rectangle = Rectangle.FromLTRB(0, 0, 1, 2);

        GroupInfo oGroupInfo2 = new GroupInfo();
        oGroupInfo2.Rectangle = Rectangle.FromLTRB(0, 0, 3, 4);

        oGraph.SetValue(ReservedMetadataKeys.GroupLayoutDrawingInfo,
            new GroupLayoutDrawingInfo(
                new GroupInfo[] {oGroupInfo1, oGroupInfo2},
                GroupRectanglePenWidth, null
            ) );

        GroupMetadataManager.TransformGroupRectangles(oGraph,
            new LayoutContext( Rectangle.FromLTRB(0, 0, 10, 20) ),
            new LayoutContext( Rectangle.FromLTRB(0, 0, 20, 60) )
            );

        GroupLayoutDrawingInfo oGroupLayoutDrawingInfo;

        Assert.IsTrue( GroupMetadataManager.TryGetGroupLayoutDrawingInfo(
            oGraph, out oGroupLayoutDrawingInfo) );

        Assert.AreEqual(2, oGroupLayoutDrawingInfo.GroupsToDraw.Count);

        Assert.AreEqual(Rectangle.FromLTRB(0, 0, 2, 6),
            oGroupLayoutDrawingInfo.GroupsToDraw[0].Rectangle);

        Assert.AreEqual(Rectangle.FromLTRB(0, 0, 6, 12),
            oGroupLayoutDrawingInfo.GroupsToDraw[1].Rectangle);
    }
    TestSaveGraph()
    {
        // Directed and undirected graphs.

        foreach (Boolean bDirected in TestGraphUtil.AllBoolean)
        {
            IGraph oGraph = new Graph(bDirected ? GraphDirectedness.Directed
                : GraphDirectedness.Undirected);

            IVertexCollection oVertices = oGraph.Vertices;
            IEdgeCollection oEdges = oGraph.Edges;

            IVertex oVertex1 = oVertices.Add();
            oVertex1.Name = "Vertex1";
            oVertex1.SetValue("VertexAttribute1", 123);  // Int32

            IVertex oVertex2 = oVertices.Add();
            oVertex2.Name = "Vertex2";
            oVertex2.SetValue("VertexAttribute2", "abc");  // String

            IVertex oVertex3 = oVertices.Add();
            oVertex3.Name = "Vertex3";
            oVertex3.SetValue("VertexAttribute1", 4.0);  // Double
            oVertex3.SetValue("VertexAttribute2", 23456.0F);  // Single

            IVertex oVertex4 = oVertices.Add();
            oVertex4.Name = "Vertex4";

            IVertex oVertex5 = oVertices.Add();
            oVertex5.Name = "Vertex5";

            IEdge oEdge;

            oEdge = oEdges.Add(oVertex1, oVertex2, bDirected);
            oEdge.SetValue("EdgeAttribute1", "ea1");

            oEdge = oEdges.Add(oVertex3, oVertex4, bDirected);
            oEdge.SetValue("EdgeAttribute2", "ea2");

            oGraph.SetValue( ReservedMetadataKeys.AllVertexMetadataKeys,
                new String [] {
                    "VertexAttribute1",
                    "VertexAttribute2",
                    } );

            oGraph.SetValue(ReservedMetadataKeys.AllEdgeMetadataKeys,
                new String [] {
                    "EdgeAttribute1",
                    "EdgeAttribute2",
                    } );

            m_oGraphAdapter.SaveGraph(oGraph, m_sTempFileName);

            String sFileContents = FileUtil.ReadTextFile(m_sTempFileName);

            XmlDocument oXmlDocument = new XmlDocument();
            oXmlDocument.LoadXml(sFileContents);

            XmlNamespaceManager oXmlNamespaceManager = new XmlNamespaceManager(
                oXmlDocument.NameTable);

            oXmlNamespaceManager.AddNamespace("g",
                GraphMLGraphAdapter.GraphMLNamespaceUri);

            String [] asRequiredXPaths = new String [] {

                // Graph node.

                String.Format("/g:graphml/g:graph[@edgedefault='{0}']",
                    bDirected ? "directed" : "undirected"),

                "/g:graphml/g:key[@id='V-VertexAttribute1' and @for='node'"
                    + " and @attr.name='VertexAttribute1'"
                    + " and @attr.type='string']",


                // Vertex nodes.

                "/g:graphml/g:key[@id='V-VertexAttribute2' and @for='node'"
                    + " and @attr.name='VertexAttribute2'"
                    + " and @attr.type='string']",

                "/g:graphml/g:key[@id='E-EdgeAttribute1' and @for='edge'"
                    + " and @attr.name='EdgeAttribute1'"
                    + " and @attr.type='string']",

                "/g:graphml/g:key[@id='E-EdgeAttribute2' and @for='edge'"
                    + " and @attr.name='EdgeAttribute2'"
                    + " and @attr.type='string']",

                "/g:graphml/g:graph/g:node[@id='Vertex1']/"
                    + "g:data[@key='V-VertexAttribute1' and .='123']",

                "/g:graphml/g:graph/g:node[@id='Vertex2']/"
                    + "g:data[@key='V-VertexAttribute2' and .='abc']",

                "/g:graphml/g:graph/g:node[@id='Vertex3']/"
                    + "g:data[@key='V-VertexAttribute1' and .='4']",

                "/g:graphml/g:graph/g:node[@id='Vertex3']/"
                    + "g:data[@key='V-VertexAttribute2' and .='23456']",

                "/g:graphml/g:graph/g:node[@id='Vertex4']",

                "/g:graphml/g:graph/g:node[@id='Vertex5']",


                // Edge nodes.

                "/g:graphml/g:graph/g:edge[@source='Vertex1' and"
                    + " @target='Vertex2']/"
                    + "g:data[@key='E-EdgeAttribute1' and .='ea1']",

                "/g:graphml/g:graph/g:edge[@source='Vertex3' and"
                    + " @target='Vertex4']/"
                    + "g:data[@key='E-EdgeAttribute2' and .='ea2']",
                };

            foreach (String sRequiredXPath in asRequiredXPaths)
            {
                XmlNode oXmlNode = oXmlDocument.SelectSingleNode(
                    sRequiredXPath, oXmlNamespaceManager);

                Assert.IsNotNull(oXmlNode);
            }
        }
    }