Represents an XML document containing GraphML that represents a graph.
See the "GraphML Primer" for details on the GraphML XML schema:

http://graphml.graphdrawing.org/primer/graphml-primer.html

Creating a GraphMLXmlDocument automatically creates an XML declaration, a root "graphml" XML node, and a "graph" child XML node. Use DefineGraphMLAttribute, AppendVertexXmlNode, AppendEdgeXmlNode, and to populate the document with vertices, edges, and vertex/edge attributes.

Inheritance: System.Xml.XmlDocument
    CreateGraphMLXmlDocument
    (
        Boolean includeStatistics,
        Boolean includeStatuses
    )
    {
        GraphMLXmlDocument graphMLXmlDocument = new GraphMLXmlDocument(true);

        if (includeStatistics)
        {
            TwitterGraphMLUtil.DefineVertexStatisticsGraphMLAttributes(
                graphMLXmlDocument);
        }

        TwitterGraphMLUtil.DefineCommonGraphMLAttributes(graphMLXmlDocument);

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexTweetedSearchTermID, "Tweeted Search Term?");

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexToolTipID, "Tooltip");

        if (includeStatuses)
        {
            TwitterGraphMLUtil.DefineEdgeStatusGraphMLAttributes(
                graphMLXmlDocument);
        }

        return (graphMLXmlDocument);
    }
    DefineVertexImageFileGraphMLAttribute
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexImageFileID, VertexImageFileColumnName);
    }
    DefineVertexLabelGraphMLAttribute
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexLabelID, VertexLabelColumnName);
    }
    DefineEdgeRelationshipGraphMLAttribute
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineEdgeStringGraphMLAttributes(
            EdgeRelationshipID, "Relationship");
    }
    DefineVertexLatestStatusGraphMLAttributes
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexLatestStatusID, "Latest Tweet",
            VertexLatestStatusUrlsID, "URLs in Latest Tweet",
            VertexLatestStatusDomainsID, "Domains in Latest Tweet",
            VertexLatestStatusHashtagsID, "Hashtags in Latest Tweet",
            VertexLatestStatusDateUtcID, "Latest Tweet Date (UTC)"
            );

        DefineLatitudeAndLongitudeGraphMLAttributes(graphMLXmlDocument, false);
        DefineInReplyToStatusIDGraphMLAttribute(graphMLXmlDocument, false);
    }
    DefineVertexStatisticsGraphMLAttributes
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineGraphMLAttributes(false, "int",
            VertexFollowedID, "Followed",
            VertexFollowersID, "Followers",
            VertexStatusesID, "Tweets",
            VertexFavoritesID, "Favorites",
            VertexUtcOffsetID, "Time Zone UTC Offset (Seconds)"
            );

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexDescriptionID, "Description",
            VertexLocationID, "Location",
            VertexUrlID, "Web",
            VertexTimeZoneID, "Time Zone",
            VertexJoinedDateUtcID, "Joined Twitter Date (UTC)"
            );
    }
    AppendSharedResponderEdges
    (
        GraphMLXmlDocument oGraphMLXmlDocument,
        HashSet<String> oVideoIDs,
        Int32 iMaximumResponses,
        String sUrlPattern,
        String sResponderTitle,
        String sKeyAttributeID,
        RequestStatistics oRequestStatistics
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oVideoIDs != null);
        Debug.Assert(iMaximumResponses > 0);
        Debug.Assert( !String.IsNullOrEmpty(sUrlPattern) );
        Debug.Assert(sUrlPattern.IndexOf("{0}") >= 0);
        Debug.Assert( !String.IsNullOrEmpty(sResponderTitle) );
        Debug.Assert( !String.IsNullOrEmpty(sKeyAttributeID) );
        Debug.Assert(oRequestStatistics != null);
        AssertValid();

        // The key is the name of an author and the value is a LinkedList of
        // the video IDs to which the author has responded.

        Dictionary< String, LinkedList<String> > oAuthorUserNameDictionary =
            new Dictionary< String, LinkedList<String> >();

        foreach (String sVideoID in oVideoIDs)
        {
            ReportProgress(String.Format(
            
                "Getting {0}s for the video with the ID \"{1}\"."
                ,
                sResponderTitle,
                sVideoID
                ) );

            // This is to prevent self-loop edges that would result when the
            // same author responds to the same video more than once.

            HashSet<String> oAuthorUserNames = new HashSet<String>();

            String sUrl = String.Format(sUrlPattern, sVideoID);

            // The document consists of an "entry" XML node for each response.

            foreach ( XmlNode oEntryXmlNode in EnumerateXmlNodes(sUrl,
                "a:feed/a:entry", iMaximumResponses, true,
                oRequestStatistics) )
            {
                XmlNamespaceManager oXmlNamespaceManager =
                    CreateXmlNamespaceManager(oEntryXmlNode.OwnerDocument);

                String sAuthorUserName;

                if (
                    XmlUtil2.TrySelectSingleNodeAsString(oEntryXmlNode,
                        "a:author/a:name/text()", oXmlNamespaceManager,
                        out sAuthorUserName)
                    &&
                    !oAuthorUserNames.Contains(sAuthorUserName)
                    )
                {
                    AddVideoIDToDictionary(sAuthorUserName, sVideoID,
                        oAuthorUserNameDictionary);

                    oAuthorUserNames.Add(sAuthorUserName);
                }
            }
        }

        ReportProgress("Adding edges for shared " + sResponderTitle + "s.");

        AppendEdgesFromDictionary(oAuthorUserNameDictionary,
            oGraphMLXmlDocument, "Shared " + sResponderTitle, sKeyAttributeID);
    }
    CreateGraphMLXmlDocument
    (
        WhatToInclude eWhatToInclude
    )
    {
        AssertValid();

        GraphMLXmlDocument oGraphMLXmlDocument = new GraphMLXmlDocument(false);

        NodeXLGraphMLUtil.DefineEdgeRelationshipGraphMLAttribute(
            oGraphMLXmlDocument);

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedCategoryEdges) )
        {
            oGraphMLXmlDocument.DefineEdgeStringGraphMLAttributes(
                SharedCategoryID, "Shared Category");
        }

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedCommenterEdges) )
        {
            oGraphMLXmlDocument.DefineEdgeStringGraphMLAttributes(
                SharedCommenterID, "Shared Commenter");
        }

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedVideoResponderEdges) )
        {
            oGraphMLXmlDocument.DefineEdgeStringGraphMLAttributes(
                SharedVideoResponderID, "Shared Video Responder");
        }

        oGraphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            TitleID, "Title",
            AuthorID, "Author",
            CreatedDateUtcID, "Created Date (UTC)"
            );

        oGraphMLXmlDocument.DefineGraphMLAttribute(false, RatingID,
            "Rating", "double", null);

        oGraphMLXmlDocument.DefineGraphMLAttributes(false, "int",
            ViewsID, "Views",
            FavoritedID, "Favorited",
            CommentsID, "Comments"
            );

        NodeXLGraphMLUtil.DefineVertexImageFileGraphMLAttribute(
            oGraphMLXmlDocument);

        NodeXLGraphMLUtil.DefineVertexCustomMenuGraphMLAttributes(
            oGraphMLXmlDocument);

        return (oGraphMLXmlDocument);
    }
    AppendVertexXmlNodes
    (
        String sSearchTerm,
        WhatToInclude eWhatToInclude,
        Int32 iMaximumVideos,
        GraphMLXmlDocument oGraphMLXmlDocument,
        RequestStatistics oRequestStatistics,
        out HashSet<String> oVideoIDs,
        out Dictionary< String, LinkedList<String> > oCategoryDictionary
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
        Debug.Assert(iMaximumVideos> 0);
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oRequestStatistics != null);
        AssertValid();

        ReportProgress("Getting a list of videos.");

        // This is used to skip duplicate videos in the results returned by
        // YouTube.  (I'm not sure why YouTube sometimes returns duplicates,
        // but it does.)

        oVideoIDs = new HashSet<String>();

        // If an edge should be included for each pair of videos that share the
        // same category, the key is a lower-case category and the value is a
        // LinkedList of the video IDs that have the category.

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedCategoryEdges) )
        {
            oCategoryDictionary =
                new Dictionary< String, LinkedList<String> >();
        }
        else
        {
            oCategoryDictionary = null;
        }

        String sUrl = String.Format(

            "http://gdata.youtube.com/feeds/api/videos?q={0}"
            ,
            EncodeUrlParameter(sSearchTerm)
            );

        // The document consists of an "entry" XML node for each video.

        foreach ( XmlNode oEntryXmlNode in EnumerateXmlNodes(sUrl,
            "a:feed/a:entry", iMaximumVideos, false, oRequestStatistics) )
        {
            XmlNamespaceManager oXmlNamespaceManager =
                CreateXmlNamespaceManager(oEntryXmlNode.OwnerDocument);

            // Use the video ID as the GraphML vertex name.  The video title
            // can't be used because it is not unique.

            String sVideoID;

            if (
                !XmlUtil2.TrySelectSingleNodeAsString(oEntryXmlNode,
                    "media:group/yt:videoid/text()", oXmlNamespaceManager,
                    out sVideoID)
                ||
                oVideoIDs.Contains(sVideoID)
                )
            {
                continue;
            }

            oVideoIDs.Add(sVideoID);

            XmlNode oVertexXmlNode = oGraphMLXmlDocument.AppendVertexXmlNode(
                sVideoID);

            AppendStringGraphMLAttributeValue(oEntryXmlNode,
                "a:title/text()", oXmlNamespaceManager, oGraphMLXmlDocument,
                oVertexXmlNode, TitleID);

            AppendStringGraphMLAttributeValue(oEntryXmlNode,
                "a:author/a:name/text()", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, AuthorID);

            AppendDoubleGraphMLAttributeValue(oEntryXmlNode,
                "gd:rating/@average", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, RatingID);

            AppendInt32GraphMLAttributeValue(oEntryXmlNode,
                "yt:statistics/@viewCount", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, ViewsID);

            AppendInt32GraphMLAttributeValue(oEntryXmlNode,
                "yt:statistics/@favoriteCount", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, FavoritedID);

            AppendInt32GraphMLAttributeValue(oEntryXmlNode,
                "gd:comments/gd:feedLink/@countHint", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, CommentsID);

            AppendYouTubeDateGraphMLAttributeValue(oEntryXmlNode,
                "a:published/text()", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, CreatedDateUtcID);

            AppendStringGraphMLAttributeValue(oEntryXmlNode,
                "media:group/media:thumbnail/@url", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode,
                NodeXLGraphMLUtil.VertexImageFileID);

            if ( AppendStringGraphMLAttributeValue(oEntryXmlNode,
                    "media:group/media:player/@url", oXmlNamespaceManager,
                    oGraphMLXmlDocument, oVertexXmlNode,
                    NodeXLGraphMLUtil.VertexMenuActionID) )
            {
                oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                    NodeXLGraphMLUtil.VertexMenuTextID,
                    "Play Video in Browser");
            }

            if (oCategoryDictionary != null)
            {
                CollectCategories(oEntryXmlNode, sVideoID,
                    oXmlNamespaceManager, oCategoryDictionary);
            }
        }
    }
    DefineLabelGraphMLAttribute
    (
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        oGraphMLXmlDocument.DefineGraphMLAttribute(false, LabelID,
            LabelColumnName, "string", null);
    }
    GetVideoNetworkInternal
    (
        String sSearchTerm,
        WhatToInclude eWhatToInclude,
        Int32 iMaximumVideos,
        RequestStatistics oRequestStatistics,
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
        Debug.Assert(iMaximumVideos > 0);
        Debug.Assert(oRequestStatistics != null);
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        // First, add a vertex for each video matching the search term.

        HashSet<String> oVideoIDs;
        Dictionary< String, LinkedList<String> > oCategoryDictionary;

        AppendVertexXmlNodes(sSearchTerm, eWhatToInclude, iMaximumVideos,
            oGraphMLXmlDocument, oRequestStatistics, out oVideoIDs,
            out oCategoryDictionary);

        // Now add whatever edges were requested.

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedCategoryEdges) )
        {
            Debug.Assert(oCategoryDictionary != null);

            ReportProgress("Adding edges for shared categories.");

            AppendEdgesFromDictionary(oCategoryDictionary, oGraphMLXmlDocument,
                "Shared category", SharedCategoryID);
        }

        oCategoryDictionary = null;

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedCommenterEdges) )
        {
            AppendSharedResponderEdges(oGraphMLXmlDocument, oVideoIDs,
                MaximumCommentsPerVideo,
                "http://gdata.youtube.com/feeds/api/videos/{0}/comments",
                "commenter", SharedCommenterID, oRequestStatistics);
        }

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedVideoResponderEdges) )
        {
            AppendSharedResponderEdges(oGraphMLXmlDocument, oVideoIDs,
                iMaximumVideos,
                "http://gdata.youtube.com/feeds/api/videos/{0}/responses",
                "video responder", SharedVideoResponderID, oRequestStatistics);
        }
    }
    DefineImportedIDGraphMLAttribute
    (
        GraphMLXmlDocument graphMLXmlDocument,
        Boolean forEdges
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineStringGraphMLAttributes(forEdges,
            ImportedIDID, "Imported ID");
    }
    AppendVertexXmlNode
    (
        String sScreenName,
        String sUserID,
        GraphMLXmlDocument oGraphMLXmlDocument,
        Dictionary<String, TwitterUser> oUserIDDictionary,
        Dictionary<String, Object> oUserValueDictionary,
        Boolean bIncludeStatistics,
        Boolean bIncludeLatestStatus,
        Boolean bExpandLatestStatusUrls
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sScreenName) );
        Debug.Assert( !String.IsNullOrEmpty(sUserID) );
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oUserIDDictionary != null);
        Debug.Assert(oUserValueDictionary != null);
        AssertValid();

        TwitterUser oTwitterUser;

        TwitterGraphMLUtil.TryAppendVertexXmlNode(sScreenName, sUserID,
            oGraphMLXmlDocument, oUserIDDictionary, out oTwitterUser);

        AppendUserInformationFromValueDictionary(oUserValueDictionary,
            oGraphMLXmlDocument, oTwitterUser, bIncludeStatistics,
            bIncludeLatestStatus, bExpandLatestStatusUrls);
    }
    AppendVertexXmlNodesForSearchTerm
    (
        String sSearchTerm,
        WhatToInclude eWhatToInclude,
        Int32 iMaximumStatuses,
        GraphMLXmlDocument oGraphMLXmlDocument,
        Dictionary<String, TwitterUser> oUserIDDictionary,
        RequestStatistics oRequestStatistics
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
        Debug.Assert(iMaximumStatuses > 0);
        Debug.Assert(iMaximumStatuses != Int32.MaxValue);
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oUserIDDictionary != null);
        Debug.Assert(oRequestStatistics != null);
        AssertValid();

        Boolean bExpandStatusUrls = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.ExpandedStatusUrls);

        Boolean bIncludeStatistics = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.Statistics);

        ReportProgress("Getting a list of tweets.");

        // Get the tweets that contain the search term.  Note that multiple
        // tweets may have the same author.

        Debug.Assert(m_oTwitterUtil != null);

        foreach ( Dictionary<String, Object> oStatusValueDictionary in

            m_oTwitterUtil.EnumerateSearchStatuses(
                sSearchTerm, iMaximumStatuses, oRequestStatistics,
                new ReportProgressHandler(this.ReportProgress),

                new CheckCancellationPendingHandler(
                    this.CheckCancellationPending)
                ) )
        {
            const String UserKeyName = "user";

            if ( !oStatusValueDictionary.ContainsKey(UserKeyName) )
            {
                // This has actually happened--Twitter occasionally sends a
                // status without user information.

                continue;
            }

            Dictionary<String, Object> oUserValueDictionary =
                ( Dictionary<String, Object> )
                oStatusValueDictionary[UserKeyName];

            TwitterUser oTwitterUser;

            if ( !TwitterSearchNetworkGraphMLUtil.
                TryAppendVertexXmlNode(oUserValueDictionary,
                    bIncludeStatistics, true, oGraphMLXmlDocument,
                    oUserIDDictionary, out oTwitterUser) )
            {
                continue;
            }

            // Parse the status and add it to the user's status collection.

            if ( !TwitterSearchNetworkGraphMLUtil.TryAddStatusToUser(
                oStatusValueDictionary, oTwitterUser, bExpandStatusUrls) )
            {
                continue;
            }
        }
    }
    OnNetworkObtainedWithoutTerminatingException
    (
        GraphMLXmlDocument oGraphMLXmlDocument,
        RequestStatistics oRequestStatistics,
        String sNetworkDescription
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oRequestStatistics != null);
        Debug.Assert( !String.IsNullOrEmpty(sNetworkDescription) );
        AssertValid();

        XmlUtil2.SetAttributes(oGraphMLXmlDocument.GraphXmlNode, "description",
            sNetworkDescription);

        if (oRequestStatistics.UnexpectedExceptions > 0)
        {
            // The network is partial.

            throw new PartialNetworkException(oGraphMLXmlDocument,
                oRequestStatistics);
        }
    }
    AppendEdgeXmlNode
    (
        GraphMLXmlDocument oGraphMLXmlDocument,
        String sVertex1ID,
        String sVertex2ID,
        String sRelationship
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert( !String.IsNullOrEmpty(sVertex1ID) );
        Debug.Assert( !String.IsNullOrEmpty(sVertex2ID) );
        Debug.Assert( !String.IsNullOrEmpty(sRelationship) );
        AssertValid();

        XmlNode oEdgeXmlNode = oGraphMLXmlDocument.AppendEdgeXmlNode(
            sVertex1ID, sVertex2ID);

        oGraphMLXmlDocument.AppendGraphMLAttributeValue(oEdgeXmlNode,
            RelationshipID, sRelationship);

        return (oEdgeXmlNode);
    }
    GetNetworkInternal
    (
        String sSearchTerm,
        WhatToInclude eWhatToInclude,
        Int32 iMaximumStatuses,
        RequestStatistics oRequestStatistics,
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
        Debug.Assert(iMaximumStatuses > 0);
        Debug.Assert(iMaximumStatuses != Int32.MaxValue);
        Debug.Assert(oRequestStatistics != null);
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        // The key is the Twitter user ID and the value is the corresponding
        // TwitterUser.

        Dictionary<String, TwitterUser> oUserIDDictionary =
            new Dictionary<String, TwitterUser>();

        // First, append a vertex for each person who has tweeted the search
        // term.

        AppendVertexXmlNodesForSearchTerm(sSearchTerm, eWhatToInclude,
            iMaximumStatuses, oGraphMLXmlDocument, oUserIDDictionary,
            oRequestStatistics);

        // Now append a vertex for each person who was mentioned or replied to
        // by the first set of people, but who didn't tweet the search term
        // himself.

        AppendVertexXmlNodesForMentionsAndRepliesTo(eWhatToInclude,
            oGraphMLXmlDocument, oUserIDDictionary, oRequestStatistics);

        TwitterSearchNetworkGraphMLUtil.AppendVertexTooltipXmlNodes(
            oGraphMLXmlDocument, oUserIDDictionary);

        if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.FollowedEdges) )
        {
            // Look at the people followed by each author, and if a followed
            // has also tweeted the search term, add an edge between the author
            // and the followed.

            AppendFollowedOrFollowingEdgeXmlNodes(oUserIDDictionary, true,
                MaximumFollowers, oGraphMLXmlDocument, oRequestStatistics);
        }

        AppendRepliesToAndMentionsEdgeXmlNodes(oGraphMLXmlDocument,
            oUserIDDictionary.Values,

            TwitterGraphMLUtil.TwitterUsersToUniqueScreenNames(
                oUserIDDictionary.Values),

            WhatToIncludeFlagIsSet(eWhatToInclude,
                WhatToInclude.RepliesToEdges),

            WhatToIncludeFlagIsSet(eWhatToInclude,
                WhatToInclude.MentionsEdges),

            WhatToIncludeFlagIsSet(eWhatToInclude,
                WhatToInclude.NonRepliesToNonMentionsEdges),

            WhatToIncludeFlagIsSet(eWhatToInclude,
                WhatToInclude.Statuses)
            );
    }
    AppendDoubleGraphMLAttributeValue
    (
        XmlNode oXmlNodeToSelectFrom,
        String sXPath,
        XmlNamespaceManager oXmlNamespaceManager,
        GraphMLXmlDocument oGraphMLXmlDocument,
        XmlNode oEdgeOrVertexXmlNode,
        String sGraphMLAttributeID
    )
    {
        Debug.Assert(oXmlNodeToSelectFrom != null);
        Debug.Assert( !String.IsNullOrEmpty(sXPath) );
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oEdgeOrVertexXmlNode != null);
        Debug.Assert( !String.IsNullOrEmpty(sGraphMLAttributeID) );
        AssertValid();

        Double dAttributeValue;

        if ( XmlUtil2.TrySelectSingleNodeAsDouble(oXmlNodeToSelectFrom, sXPath,
            oXmlNamespaceManager, out dAttributeValue) )
        {
            oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                oEdgeOrVertexXmlNode, sGraphMLAttributeID, dAttributeValue);

            return (true);
        }

        return (false);
    }
    DefineRelationshipGraphMLAttribute
    (
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        oGraphMLXmlDocument.DefineGraphMLAttribute(true, RelationshipID,
            "Relationship", "string", null);
    }
    DefineCustomMenuGraphMLAttributes
    (
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        oGraphMLXmlDocument.DefineGraphMLAttribute(false, MenuTextID,
            MenuTextColumnName, "string", null);

        oGraphMLXmlDocument.DefineGraphMLAttribute(false, MenuActionID,
            MenuActionColumnName, "string", null);
    }
    AppendEdgesFromDictionary
    (
        Dictionary< String, LinkedList<String> > oDictionary,
        GraphMLXmlDocument oGraphMLXmlDocument,
        String sRelationship,
        String sKeyAttributeID
    )
    {
        Debug.Assert(oDictionary != null);
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert( !String.IsNullOrEmpty(sRelationship) );
        Debug.Assert( !String.IsNullOrEmpty(sKeyAttributeID) );
        AssertValid();

        // For each key...

        foreach (KeyValuePair< String, LinkedList<String> > oKeyValuePair in
            oDictionary)
        {
            String sKey = oKeyValuePair.Key;

            // For each video ID that has the key...

            LinkedList<String> oVideoIDsWithThisKey = oKeyValuePair.Value;

            for (
                LinkedListNode<String> oVideoIDWithThisKey =
                    oVideoIDsWithThisKey.First;

                oVideoIDWithThisKey != null;

                oVideoIDWithThisKey = oVideoIDWithThisKey.Next
                )
            {
                // For each of the subsequent video IDs in the LinkedList that
                // have the key...

                for (
                    LinkedListNode<String> oSubsequentVideoIDWithThisKey =
                        oVideoIDWithThisKey.Next;

                    oSubsequentVideoIDWithThisKey != null;

                    oSubsequentVideoIDWithThisKey =
                        oSubsequentVideoIDWithThisKey.Next
                    )
                {
                    XmlNode oEdgeXmlNode = NodeXLGraphMLUtil.AppendEdgeXmlNode(
                        oGraphMLXmlDocument, oVideoIDWithThisKey.Value,
                        oSubsequentVideoIDWithThisKey.Value, sRelationship);

                    oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                        oEdgeXmlNode, sKeyAttributeID, sKey);
                }
            }
        }
    }
    AppendVertexXmlNodes
    (
        Boolean bUseListName,
        String sListName,
        ICollection<String> oScreenNames,
        WhatToInclude eWhatToInclude,
        GraphMLXmlDocument oGraphMLXmlDocument,
        Dictionary<String, TwitterUser> oUserIDDictionary,
        RequestStatistics oRequestStatistics
    )
    {
        Debug.Assert( !bUseListName || !String.IsNullOrEmpty(sListName) );
        Debug.Assert(bUseListName || oScreenNames != null);
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oUserIDDictionary != null);
        Debug.Assert(oRequestStatistics != null);
        AssertValid();

        Boolean bIncludeStatistics = WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.Statistics);

        Boolean bIncludeLatestStatuses = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.LatestStatuses);

        Boolean bExpandLatestStatusUrls = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.ExpandedLatestStatusUrls);

        String sUserID;

        if (bUseListName)
        {
            String sSlug, sOwnerScreenName;

            if ( !TryParseListName(sListName, out sSlug,
                out sOwnerScreenName) )
            {
                return;
            }

            String sUrl = String.Format(

                "{0}lists/members.json?slug={1}&owner_screen_name={2}&{3}"
                ,
                TwitterApiUrls.Rest,
                TwitterUtil.EncodeUrlParameter(sSlug),
                TwitterUtil.EncodeUrlParameter(sOwnerScreenName),
                TwitterApiUrlParameters.IncludeEntities
                );

            // The JSON contains a "users" array for the users in the Twitter
            // list.

            foreach ( Object oResult in EnumerateJsonValues(sUrl, "users",
                Int32.MaxValue, false, oRequestStatistics) )
            {
                String sScreenName;

                Dictionary<String, Object> oUserValueDictionary =
                    ( Dictionary<String, Object> )oResult;

                if (
                    TryGetScreenNameFromDictionary(oUserValueDictionary,
                        out sScreenName)
                    &&
                    TryGetUserIDFromDictionary(oUserValueDictionary,
                        out sUserID)
                    )
                {
                    AppendVertexXmlNode(sScreenName, sUserID,
                        oGraphMLXmlDocument, oUserIDDictionary,
                        oUserValueDictionary, bIncludeStatistics,
                        bIncludeLatestStatuses, bExpandLatestStatusUrls);
                }
            }
        }
        else
        {
            // Eliminate duplicate names.

            HashSet<String> oUniqueScreenNames = new HashSet<String>();

            foreach (String sScreenName in oScreenNames)
            {
                oUniqueScreenNames.Add( sScreenName.ToLower() );
            }

            foreach (String sScreenName in oUniqueScreenNames)
            {
                Dictionary<String, Object> oUserValueDictionary;

                if (
                    TryGetUserValueDictionary(sScreenName, oRequestStatistics,
                        true, out oUserValueDictionary)
                    &&
                    TryGetUserIDFromDictionary(oUserValueDictionary,
                        out sUserID)
                    )
                {
                    AppendVertexXmlNode(sScreenName, sUserID,
                        oGraphMLXmlDocument, oUserIDDictionary,
                        oUserValueDictionary, bIncludeStatistics,
                        bIncludeLatestStatuses, bExpandLatestStatusUrls);
                }
            }
        }
    }
    DefineVertexCustomMenuGraphMLAttributes
    (
        GraphMLXmlDocument graphMLXmlDocument
    )
    {
        Debug.Assert(graphMLXmlDocument != null);

        graphMLXmlDocument.DefineVertexStringGraphMLAttributes(
            VertexMenuTextID, VertexMenuTextColumnName, 
            VertexMenuActionID, VertexMenuActionColumnName
            );
    }
    SaveGraphCore
    (
        IGraph graph,
        Stream stream
    )
    {
        Debug.Assert(graph != null);
        Debug.Assert(stream != null);
        AssertValid();

        GraphMLXmlDocument oGraphMLXmlDocument = new GraphMLXmlDocument(
            graph.Directedness == GraphDirectedness.Directed);

        String [] asEdgeAttributeNames = GetAttributeNames(graph, false);
        String [] asVertexAttributeNames = GetAttributeNames(graph, true);

        // Define the GraphML-attributes.

        const String VertexAttributeIDPrefix = "V-";
        const String EdgeAttributeIDPrefix = "E-";

        foreach (String sVertexAttributeName in asVertexAttributeNames)
        {
            oGraphMLXmlDocument.DefineGraphMLAttribute(false,
                VertexAttributeIDPrefix + sVertexAttributeName,
                sVertexAttributeName, "string", null);
        }

        foreach (String sEdgeAttributeName in asEdgeAttributeNames)
        {
            oGraphMLXmlDocument.DefineGraphMLAttribute(true,
                EdgeAttributeIDPrefix + sEdgeAttributeName,
                sEdgeAttributeName, "string", null);
        }

        // Add the vertices and their GraphML-attribute values.

        foreach (IVertex oVertex in graph.Vertices)
        {
            XmlNode oVertexXmlNode = oGraphMLXmlDocument.AppendVertexXmlNode(
                oVertex.Name);

            AppendGraphMLAttributeValues(oVertex, oGraphMLXmlDocument,
                oVertexXmlNode, asVertexAttributeNames,
                VertexAttributeIDPrefix);
        }

        // Add the edges and their GraphML-attribute values.

        foreach (IEdge oEdge in graph.Edges)
        {
            IVertex [] oVertices = oEdge.Vertices;

            XmlNode oEdgeXmlNode = oGraphMLXmlDocument.AppendEdgeXmlNode(
                oVertices[0].Name, oVertices[1].Name);

            AppendGraphMLAttributeValues(oEdge, oGraphMLXmlDocument,
                oEdgeXmlNode, asEdgeAttributeNames, EdgeAttributeIDPrefix);
        }

        oGraphMLXmlDocument.Save(stream);
    }
    AppendEdgeXmlNode
    (
        GraphMLXmlDocument graphMLXmlDocument,
        String vertex1ID,
        String vertex2ID,
        String relationship
    )
    {
        Debug.Assert(graphMLXmlDocument != null);
        Debug.Assert( !String.IsNullOrEmpty(vertex1ID) );
        Debug.Assert( !String.IsNullOrEmpty(vertex2ID) );
        Debug.Assert( !String.IsNullOrEmpty(relationship) );

        XmlNode edgeXmlNode = graphMLXmlDocument.AppendEdgeXmlNode(
            vertex1ID, vertex2ID);

        graphMLXmlDocument.AppendGraphMLAttributeValue(edgeXmlNode,
            EdgeRelationshipID, relationship);

        return (edgeXmlNode);
    }
    AppendGraphMLAttributeValues
    (
        IMetadataProvider oEdgeOrVertex,
        GraphMLXmlDocument oGraphMLXmlDocument,
        XmlNode oEdgeOrVertexXmlNode,
        String [] asAttributeNames,
        String AttributeIDPrefix
    )
    {
        Debug.Assert(oEdgeOrVertex != null);
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oEdgeOrVertexXmlNode != null);
        Debug.Assert(asAttributeNames != null);
        Debug.Assert( !String.IsNullOrEmpty(AttributeIDPrefix) );
        AssertValid();

        foreach (String sAttributeName in asAttributeNames)
        {
            Object oAttributeValue;

            // Note that the value type isn't checked.  Whatever type it is,
            // it gets converted to a string.

            if (oEdgeOrVertex.TryGetValue(sAttributeName,
                out oAttributeValue) && oAttributeValue != null)
            {
                oGraphMLXmlDocument.AppendGraphMLAttributeValue( 
                    oEdgeOrVertexXmlNode, AttributeIDPrefix + sAttributeName,
                    oAttributeValue.ToString() );
            }
        }
    }
    AppendVertexXmlNodesForMentionsAndRepliesTo
    (
        WhatToInclude eWhatToInclude,
        GraphMLXmlDocument oGraphMLXmlDocument,
        Dictionary<String, TwitterUser> oUserIDDictionary,
        RequestStatistics oRequestStatistics
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oUserIDDictionary != null);
        Debug.Assert(oRequestStatistics != null);
        AssertValid();

        ReportProgress("Getting replied to and mentioned users.");

        // Get the screen names that were mentioned or replied to by the people
        // who tweeted the search term.

        String[] asUniqueMentionsAndRepliesToScreenNames =
            TwitterSearchNetworkGraphMLUtil.GetMentionsAndRepliesToScreenNames(
                oUserIDDictionary);

        Boolean bIncludeStatistics = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.Statistics);

        // Get information about each of those screen names and append vertex
        // XML nodes for them if necessary.

        foreach ( Dictionary<String, Object> oUserValueDictionary in
            EnumerateUserValueDictionaries(
                asUniqueMentionsAndRepliesToScreenNames, false,
                oRequestStatistics) )
        {
            TwitterUser oTwitterUser;

            TwitterSearchNetworkGraphMLUtil.TryAppendVertexXmlNode(
                oUserValueDictionary, bIncludeStatistics, false,
                oGraphMLXmlDocument, oUserIDDictionary, out oTwitterUser);
        }
    }
    DefineImageFileGraphMLAttribute
    (
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        oGraphMLXmlDocument.DefineGraphMLAttribute(false, ImageFileID,
            ImageColumnName, "string", null);
    }
    AppendYouTubeDateGraphMLAttributeValue
    (
        XmlNode oXmlNodeToSelectFrom,
        String sXPath,
        XmlNamespaceManager oXmlNamespaceManager,
        GraphMLXmlDocument oGraphMLXmlDocument,
        XmlNode oVertexXmlNode,
        String sGraphMLAttributeID
    )
    {
        Debug.Assert(oXmlNodeToSelectFrom != null);
        Debug.Assert( !String.IsNullOrEmpty(sXPath) );
        Debug.Assert(oGraphMLXmlDocument != null);
        Debug.Assert(oVertexXmlNode != null);
        Debug.Assert( !String.IsNullOrEmpty(sGraphMLAttributeID) );
        AssertValid();

        String sYouTubeDate;

        if ( XmlUtil2.TrySelectSingleNodeAsString(oXmlNodeToSelectFrom,
            sXPath, oXmlNamespaceManager, out sYouTubeDate) )
        {
            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                sGraphMLAttributeID, FormatYouTubeDate(sYouTubeDate) );

            return (true);
        }

        return (false);
    }
    GetNetworkDescription
    (
        String sSearchTerm,
        WhatToInclude eWhatToInclude,
        Int32 iMaximumStatuses,
        GraphMLXmlDocument oGraphMLXmlDocument
    )
    {
        Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
        Debug.Assert(iMaximumStatuses > 0);
        Debug.Assert(iMaximumStatuses != Int32.MaxValue);
        Debug.Assert(oGraphMLXmlDocument != null);
        AssertValid();

        const String Int32FormatString = "N0";

        NetworkDescriber oNetworkDescriber = new NetworkDescriber();
        Int32 iVertexXmlNodes = oGraphMLXmlDocument.VertexXmlNodes;

        oNetworkDescriber.AddSentence(

            // WARNING:
            //
            // If you change this first sentence, you may also have to change
            // the code in
            // TwitterSearchNetworkTopItemsCalculator2.GetSearchTerm(), which
            // attempts to extract the search term from the graph description.

            "The graph represents a network of {0} Twitter {1} whose recent"
            + " tweets contained \"{2}\", or who {3} replied to or mentioned"
            + " in those tweets, taken from a data set limited to a maximum of"
            + " {4} tweets."
            ,
            iVertexXmlNodes.ToString(Int32FormatString),
            StringUtil.MakePlural("user", iVertexXmlNodes),
            sSearchTerm,
            iVertexXmlNodes > 1 ? "were" : "was",
            iMaximumStatuses.ToString(Int32FormatString)
            );

        oNetworkDescriber.AddNetworkTime(NetworkSource);

        Boolean bIncludeFollowedEdges = WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.FollowedEdges);

        Boolean bIncludeRepliesToEdges = WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.RepliesToEdges);

        Boolean bIncludeMentionsEdges = WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.MentionsEdges);

        Boolean bIncludeNonRepliesToNonMentionsEdges = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.NonRepliesToNonMentionsEdges);

        if (bIncludeRepliesToEdges && bIncludeMentionsEdges &&
            bIncludeNonRepliesToNonMentionsEdges)
        {
            // Every collected tweet has an edge that contains the date of the
            // tweet, so the range of tweet dates can be determined.

            oNetworkDescriber.StartNewParagraph();

            TweetDateRangeAnalyzer.AddTweetDateRangeToNetworkDescription(
                oGraphMLXmlDocument, oNetworkDescriber);
        }

        if (bIncludeFollowedEdges || bIncludeRepliesToEdges ||
            bIncludeMentionsEdges || bIncludeNonRepliesToNonMentionsEdges)
        {
            oNetworkDescriber.StartNewParagraph();
        }

        if (bIncludeFollowedEdges)
        {
            oNetworkDescriber.AddSentence(
                "There is an edge for each follows relationship."
                );
        }

        if (bIncludeRepliesToEdges)
        {
            oNetworkDescriber.AddSentence(
                "There is an edge for each \"replies-to\" relationship in a"
                + " tweet."
                );
        }

        if (bIncludeMentionsEdges)
        {
            oNetworkDescriber.AddSentence(
                "There is an edge for each \"mentions\" relationship in a"
                + " tweet."
                );
        }

        if (bIncludeNonRepliesToNonMentionsEdges)
        {
            oNetworkDescriber.AddSentence(
                "There is a self-loop edge for each tweet that is not a"
                + " \"replies-to\" or \"mentions\"."
                );
        }

        return ( oNetworkDescriber.ConcatenateSentences() );
    }