//*************************************************************************
        //  Method: AppendVertexXmlNodes()
        //
        /// <summary>
        /// Appends a vertex XML node for each video that matches a specified
        /// search term.
        /// </summary>
        ///
        /// <param name="sSearchTerm">
        /// The term to search for.
        /// </param>
        ///
        /// <param name="eWhatToInclude">
        /// Specifies what should be included in the network.
        /// </param>
        ///
        /// <param name="iMaximumVideos">
        /// Maximum number of videos to request, or Int32.MaxValue for no limit.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// The GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        ///
        /// <param name="oVideoIDs">
        /// Where the set of unique video IDs gets stored.
        /// </param>
        ///
        /// <param name="oTagDictionary">
        /// If an edge should be included for each pair of videos that share the
        /// same tag, this gets set to a Dictionary for which the key is a
        /// lower-case tag and the value is a LinkedList of the video IDs that have
        /// the tag.  Otherwise, it gets set to null.
        /// </param>
        //*************************************************************************
        protected void AppendVertexXmlNodes(
            String sSearchTerm,
            WhatToInclude eWhatToInclude,
            Int32 iMaximumVideos,
            GraphMLXmlDocument oGraphMLXmlDocument,
            RequestStatistics oRequestStatistics,
            out HashSet<String> oVideoIDs,
            out Dictionary< String, LinkedList<String> > oTagDictionary
            )
        {
            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 tag, the key is a lower-case tag and the value is a LinkedList
            // of the video IDs that have the tag.

            if ( WhatToIncludeFlagIsSet(eWhatToInclude,
            WhatToInclude.SharedTagEdges) )
            {
            oTagDictionary = new Dictionary< String, LinkedList<String> >();
            }
            else
            {
            oTagDictionary = 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);

            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, ImageFileID);

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

            if (oTagDictionary != null)
            {
                CollectTags(oEntryXmlNode, sVideoID, oXmlNamespaceManager,
                    oTagDictionary);
            }
            }
        }
        //*************************************************************************
        //  Method: AppendSampleThumbnails()
        //
        /// <summary>
        /// Appends sample thumbnails to the GraphMLXmlDocument being populated.
        /// </summary>
        ///
        /// <param name="oTagDictionary">
        /// The key is the tag name and the value is the corresponding GraphML XML
        /// node that represents the tag.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="sApiKey">
        /// Flickr API key.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        //*************************************************************************
        protected void AppendSampleThumbnails(
            Dictionary<String, XmlNode> oTagDictionary,
            GraphMLXmlDocument oGraphMLXmlDocument,
            String sApiKey,
            RequestStatistics oRequestStatistics
            )
        {
            Debug.Assert(oTagDictionary != null);
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert( !String.IsNullOrEmpty(sApiKey) );
            Debug.Assert(oRequestStatistics != null);
            AssertValid();

            foreach (KeyValuePair<String, XmlNode> oKeyValuePair in oTagDictionary)
            {
            String sTag = oKeyValuePair.Key;

            ReportProgress("Getting sample image file for \"" + sTag + "\".");

            String sSampleImageUrl;

            if ( TryGetSampleImageUrl(sTag, sApiKey, oRequestStatistics,
                out sSampleImageUrl) )
            {
                oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                    oKeyValuePair.Value, ImageFileID, sSampleImageUrl);
            }
            }
        }
        //*************************************************************************
        //  Method: AppendVertexXmlNode()
        //
        /// <summary>
        /// Appends a vertex XML node to the document for a tag if such a node
        /// doesn't already exist.
        /// </summary>
        ///
        /// <param name="sTag">
        /// Tag to add a vertex XML node for.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oTagDictionary">
        /// The key is the tag name and the value is the corresponding GraphML XML
        /// node that represents the tag.
        /// </param>
        //*************************************************************************
        protected void AppendVertexXmlNode(
            String sTag,
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, XmlNode> oTagDictionary
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sTag) );
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oTagDictionary != null);

            if ( !oTagDictionary.ContainsKey(sTag) )
            {
            XmlNode oVertexXmlNode = oGraphMLXmlDocument.AppendVertexXmlNode(
                sTag);

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                LabelID, sTag);

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                MenuTextID, "Open Flickr Page for This Tag");

            oGraphMLXmlDocument.AppendGraphMLAttributeValue( oVertexXmlNode,
                MenuActionID,

                String.Format(
                    "http://www.flickr.com/photos/tags/{0}/"
                    ,
                    UrlUtil.EncodeUrlParameter(sTag)
                ) );

            oTagDictionary.Add(sTag, oVertexXmlNode);
            }
        }
        //*************************************************************************
        //  Method: TryAppendVertexXmlNode()
        //
        /// <summary>
        /// Appends a vertex XML node to the GraphML document for a user if such a
        /// node doesn't already exist.
        /// </summary>
        ///
        /// <param name="sUserName">
        /// User name to add a vertex XML node for.
        /// </param>
        ///
        /// <param name="oEntryXmlNode">
        /// The "entry" XML node returned by YouTube, or null if an entry node
        /// isn't available.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oUserNameDictionary">
        /// The key is the user name and the value is the corresponding GraphML XML
        /// node that represents the user.
        /// </param>
        ///
        /// <returns>
        /// true if a vertex XML node was added, false if a vertex XML node already
        /// exists.
        /// </returns>
        //*************************************************************************
        protected Boolean TryAppendVertexXmlNode(
            String sUserName,
            XmlNode oEntryXmlNode,
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, XmlNode> oUserNameDictionary
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sUserName) );
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oUserNameDictionary != null);

            XmlNode oVertexXmlNode;

            if ( oUserNameDictionary.TryGetValue(sUserName, out oVertexXmlNode) )
            {
            return (false);
            }

            oVertexXmlNode = oGraphMLXmlDocument.AppendVertexXmlNode(sUserName);
            oUserNameDictionary.Add(sUserName, oVertexXmlNode);

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
            MenuTextID, "Open YouTube Page for This Person");

            oGraphMLXmlDocument.AppendGraphMLAttributeValue( oVertexXmlNode,
            MenuActionID, String.Format(WebPageUrlPattern, sUserName) );

            return (true);
        }
        //*************************************************************************
        //  Method: AppendVertexXmlNodes()
        //
        /// <summary>
        /// Appends a vertex XML node for each person who has tweeted a specified
        /// search term.
        /// </summary>
        ///
        /// <param name="sSearchTerm">
        /// The term to search for.
        /// </param>
        ///
        /// <param name="eWhatToInclude">
        /// Specifies what should be included in the network.
        /// </param>
        ///
        /// <param name="iMaximumPeoplePerRequest">
        /// Maximum number of people to request for each query, or Int32.MaxValue
        /// for no limit.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// The GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oScreenNameDictionary">
        /// The key is the screen name in lower case and the value is the
        /// corresponding TwitterVertex.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        //*************************************************************************
        protected void AppendVertexXmlNodes(
            String sSearchTerm,
            WhatToInclude eWhatToInclude,
            Int32 iMaximumPeoplePerRequest,
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, TwitterVertex> oScreenNameDictionary,
            RequestStatistics oRequestStatistics
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sSearchTerm) );
            Debug.Assert(iMaximumPeoplePerRequest > 0);
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oScreenNameDictionary != null);
            Debug.Assert(oRequestStatistics != null);
            AssertValid();

            // Convert spaces in the search term to a plus sign.
            //
            // (Note: Don't try to use Twitter's "show_user" URL parameter in an
            // attempt to get a "user" XML node for each author.  That's not what
            // the URL parameter does.)

            String sUrl = String.Format(

            "http://search.twitter.com/search.atom?q={0}&rpp=100"
            ,
            UrlUtil.EncodeUrlParameter(sSearchTerm).Replace("%20", "+")
            );

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

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

            Boolean bIncludeStatuses = WhatToIncludeFlagIsSet(
            eWhatToInclude, WhatToInclude.Statuses);

            // The document consists of an "entry" XML node for each tweet that
            // contains the search term.  Multiple tweets may have the same author,
            // so we have to enumerate until the requested maximum number of unique
            // authors is reached.

            foreach ( XmlNode oAuthorNameXmlNode in EnumerateXmlNodes(
            sUrl, "a:feed/a:entry/a:author/a:name", "a", AtomNamespaceUri,
            15, Int32.MaxValue, true, false, oRequestStatistics) )
            {
            if (oScreenNameDictionary.Count == iMaximumPeoplePerRequest)
            {
                break;
            }

            // The author name is in this format:
            //
            // james_laker (James Laker)

            String sAuthorName;

            if ( !XmlUtil2.TrySelectSingleNodeAsString(oAuthorNameXmlNode,
                "text()", null, out sAuthorName) )
            {
                continue;
            }

            Int32 iIndexOfSpace = sAuthorName.IndexOf(' ');
            String sScreenName = sAuthorName;

            if (iIndexOfSpace != -1)
            {
                sScreenName = sAuthorName.Substring(0, iIndexOfSpace);
            }

            sScreenName = sScreenName.ToLower();

            TwitterVertex oTwitterVertex;

            if ( !TryAppendVertexXmlNode(sScreenName, null,
                oGraphMLXmlDocument, oScreenNameDictionary, bIncludeStatistics,
                false, out oTwitterVertex) )
            {
                // A tweet for the author has already been found.

                continue;
            }

            XmlNode oVertexXmlNode = oTwitterVertex.VertexXmlNode;
            XmlNode oEntryXmlNode = oAuthorNameXmlNode.ParentNode.ParentNode;

            XmlNamespaceManager oXmlNamespaceManager = new XmlNamespaceManager(
                oEntryXmlNode.OwnerDocument.NameTable);

            oXmlNamespaceManager.AddNamespace("a", AtomNamespaceUri);

            // Get the image URL and status for the tweet's author.

            AppendStringGraphMLAttributeValue(oEntryXmlNode,
                "a:link[@rel='image']/@href", oXmlNamespaceManager,
                oGraphMLXmlDocument, oVertexXmlNode, ImageFileID);

            String sStatus;

            if ( XmlUtil2.TrySelectSingleNodeAsString(oEntryXmlNode,
                "a:title/text()", oXmlNamespaceManager, out sStatus) )
            {
                String sStatusDateUtc;

                if ( XmlUtil2.TrySelectSingleNodeAsString(oEntryXmlNode,
                    "a:published/text()", oXmlNamespaceManager,
                    out sStatusDateUtc) )
                {
                    sStatusDateUtc = TwitterDateParser.ParseTwitterDate(
                        sStatusDateUtc);
                }

                oTwitterVertex.StatusForAnalysis = sStatus;
                oTwitterVertex.StatusForAnalysisDateUtc = sStatusDateUtc;

                if (bIncludeStatuses)
                {
                    oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                        oVertexXmlNode, StatusID, sStatus);

                    if ( !String.IsNullOrEmpty(sStatusDateUtc) )
                    {
                        oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                            oVertexXmlNode, StatusDateUtcID, sStatusDateUtc);
                    }
                }
            }
            }
        }
        //*************************************************************************
        //  Method: AppendEdgeXmlNode()
        //
        /// <summary>
        /// Appends an edge XML node to a GraphML document.
        /// </summary>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="sVertex1ID">
        /// ID of the edge's first vertex.
        /// </param>
        ///
        /// <param name="sVertex2ID">
        /// ID of the edge's second vertex.
        /// </param>
        ///
        /// <param name="sRelationship">
        /// The value of the edge's RelationshipID GraphML-attribute.
        /// </param>
        ///
        /// <returns>
        /// The new edge XML node.
        /// </returns>
        //*************************************************************************
        protected XmlNode 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);
        }
        //*************************************************************************
        //  Method: AppendStringGraphMLAttributeValue()
        //
        /// <summary>
        /// Appends a String GraphML-Attribute value to an edge or vertex XML node. 
        /// </summary>
        ///
        /// <param name="oXmlNodeToSelectFrom">
        /// Node to select from.
        /// </param>
        /// 
        /// <param name="sXPath">
        /// XPath expression to a String descendant of <paramref
        /// name="oXmlNodeToSelectFrom" />.
        /// </param>
        ///
        /// <param name="oXmlNamespaceManager">
        /// NamespaceManager to use, or null to not use one.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oEdgeOrVertexXmlNode">
        /// The edge or vertex XML node from <paramref
        /// name="oGraphMLXmlDocument" /> to add the GraphML attribute value to.
        /// </param>
        ///
        /// <param name="sGraphMLAttributeID">
        /// GraphML ID of the attribute.
        /// </param>
        ///
        /// <returns>
        /// true if the GraphML-Attribute was appended.
        /// </returns>
        ///
        /// <remarks>
        /// This method selects from <paramref name="oXmlNodeToSelectFrom" /> using
        /// the <paramref name="sXPath" /> expression.  If the selection is
        /// successful, the specified String value gets stored on <paramref
        /// name="oEdgeOrVertexXmlNode" /> as a Graph-ML Attribute.
        /// </remarks>
        //*************************************************************************
        protected Boolean AppendStringGraphMLAttributeValue(
            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();

            String sAttributeValue;

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

            return (true);
            }

            return (false);
        }
        //*************************************************************************
        //  Method: AppendRepliesToAndMentionsXmlNodes()
        //
        /// <summary>
        /// Appends edge XML nodes for replies-to and mentions relationships.
        /// </summary>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oScreenNameDictionary">
        /// The key is the screen name in lower case and the value is the
        /// corresponding TwitterVertex.
        /// </param>
        ///
        /// <param name="bIncludeRepliesToEdges">
        /// true to append edges for replies-to relationships.
        /// </param>
        ///
        /// <param name="bIncludeMentionsEdges">
        /// true to append edges for mentions relationships.
        /// </param>
        //*************************************************************************
        protected void AppendRepliesToAndMentionsXmlNodes(
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, TwitterVertex> oScreenNameDictionary,
            Boolean bIncludeRepliesToEdges,
            Boolean bIncludeMentionsEdges
            )
        {
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oScreenNameDictionary != null);
            AssertValid();

            if (!bIncludeRepliesToEdges && !bIncludeMentionsEdges)
            {
            return;
            }

            XmlNamespaceManager oGraphMLXmlNamespaceManager =
            oGraphMLXmlDocument.CreateXmlNamespaceManager("g");

            ReportProgress("Examining relationships.");

            // "Starts with a screen name," which means it's a "reply-to".

            Regex oReplyToRegex = new Regex(@"^@(?<ScreenName>\w+)");

            // "Contains a screen name," which means it's a "mentions".
            //
            // Note that a "reply-to" is also a "mentions."

            Regex oMentionsRegex = new Regex(@"(^|\s)@(?<ScreenName>\w+)");

            foreach (KeyValuePair<String, TwitterVertex> oKeyValuePair in
            oScreenNameDictionary)
            {
            String sScreenName = oKeyValuePair.Key;
            TwitterVertex oTwitterVertex = oKeyValuePair.Value;
            String sStatusForAnalysis = oTwitterVertex.StatusForAnalysis;

            String sStatusForAnalysisDateUtc =
                oTwitterVertex.StatusForAnalysisDateUtc;

            if ( String.IsNullOrEmpty(sStatusForAnalysis) )
            {
                continue;
            }

            if (bIncludeRepliesToEdges)
            {
                Match oReplyToMatch = oReplyToRegex.Match(sStatusForAnalysis);

                if (oReplyToMatch.Success)
                {
                    String sReplyToScreenName =
                        oReplyToMatch.Groups["ScreenName"].Value.ToLower();

                    if (
                        sReplyToScreenName != sScreenName
                        &&
                        oScreenNameDictionary.ContainsKey(sReplyToScreenName)
                        )
                    {
                        XmlNode oEdgeXmlNode = AppendEdgeXmlNode(
                            oGraphMLXmlDocument, sScreenName,
                            sReplyToScreenName, "Replies to");

                        if ( !String.IsNullOrEmpty(sStatusForAnalysisDateUtc) )
                        {
                            oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                                oEdgeXmlNode, RelationshipDateUtcID,
                                sStatusForAnalysisDateUtc);
                        }
                    }
                }
            }

            if (bIncludeMentionsEdges)
            {
                Match oMentionsMatch =
                    oMentionsRegex.Match(sStatusForAnalysis);

                while (oMentionsMatch.Success)
                {
                    String sMentionsScreenName =
                        oMentionsMatch.Groups["ScreenName"].Value.ToLower();

                    if (
                        sMentionsScreenName != sScreenName
                        &&
                        oScreenNameDictionary.ContainsKey(sMentionsScreenName)
                        )
                    {
                        XmlNode oEdgeXmlNode = AppendEdgeXmlNode(
                            oGraphMLXmlDocument, sScreenName,
                            sMentionsScreenName, "Mentions");

                        if ( !String.IsNullOrEmpty(sStatusForAnalysisDateUtc) )
                        {
                            oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                                oEdgeXmlNode, RelationshipDateUtcID,
                                sStatusForAnalysisDateUtc);
                        }
                    }

                    oMentionsMatch = oMentionsMatch.NextMatch();
                }
            }
            }
        }
예제 #9
0
        //*************************************************************************
        //  Method: AppendGraphMLAttributeValues()
        //
        /// <summary>
        /// Appends Graph-ML attribute values to a vertex or edge XML node while
        /// saving a graph.
        /// </summary>
        ///
        /// <param name="oEdgeOrVertex">
        /// The edge or vertex to read metadata from.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// The GraphML document being populated.
        /// </param>
        ///
        /// <param name="oEdgeOrVertexXmlNode">
        /// The edge or vertex XML node in the GraphML document that corresponds to
        /// <paramref name="oEdgeOrVertex" />.
        /// </param>
        ///
        /// <param name="asAttributeNames">
        /// Array of all possible Graph-ML attribute names for the edge or vertex.
        /// </param>
        ///
        /// <param name="AttributeIDPrefix">
        /// The prefix to use for each Graph-ML attribute ID.
        /// </param>
        //*************************************************************************
        protected void 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() );
            }
            }
        }
        //*************************************************************************
        //  Method: AppendRelationshipDateUtcGraphMLAttributeValue()
        //
        /// <summary>
        /// Appends a GraphML attribute value for the relationship date to an edge
        /// XML node.
        /// </summary>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oEdgeXmlNode">
        /// The edge XML node to add the Graph-ML attribute value to.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        ///
        /// <remarks>
        /// The Graph-ML attribute value is set to the start time of the network
        /// request.
        /// </remarks>
        //*************************************************************************
        protected void AppendRelationshipDateUtcGraphMLAttributeValue(
            GraphMLXmlDocument oGraphMLXmlDocument,
            XmlNode oEdgeXmlNode,
            RequestStatistics oRequestStatistics
            )
        {
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oEdgeXmlNode != null);
            Debug.Assert(oRequestStatistics != null);
            AssertValid();

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oEdgeXmlNode,
            RelationshipDateUtcID,

            TwitterDateParser.ToCultureInvariantString(
                oRequestStatistics.StartTimeUtc)
            );
        }
        //*************************************************************************
        //  Method: AppendFromUserXmlNode()
        //
        /// <summary>
        /// Appends GraphML-Attribute values from a "user" XML node returned by
        /// Twitter to a vertex XML node.
        /// </summary>
        ///
        /// <param name="oUserXmlNode">
        /// The "user" XML node returned by Twitter.  Can't be null.
        /// </param>
        /// 
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oTwitterVertex">
        /// Contains the vertex XML node from <paramref
        /// name="oGraphMLXmlDocument" /> to add the GraphML attribute values to.
        /// </param>
        ///
        /// <param name="bIncludeStatistics">
        /// true to include the user's statistics.
        /// </param>
        ///
        /// <param name="bIncludeLatestStatus">
        /// true to include a latest status attribute value.
        /// </param>
        ///
        /// <remarks>
        /// This method reads information from a "user" XML node returned by
        /// Twitter and appends the information to a vertex XML node in the GraphML
        /// document.
        /// </remarks>
        //*************************************************************************
        protected void AppendFromUserXmlNode(
            XmlNode oUserXmlNode,
            GraphMLXmlDocument oGraphMLXmlDocument,
            TwitterVertex oTwitterVertex,
            Boolean bIncludeStatistics,
            Boolean bIncludeLatestStatus
            )
        {
            Debug.Assert(oUserXmlNode != null);
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oTwitterVertex != null);
            AssertValid();

            XmlNode oVertexXmlNode = oTwitterVertex.VertexXmlNode;

            if (bIncludeStatistics)
            {
            AppendInt32GraphMLAttributeValue(oUserXmlNode,
                "friends_count/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, FollowedID);

            AppendInt32GraphMLAttributeValue(oUserXmlNode,
                "followers_count/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, FollowersID);

            AppendInt32GraphMLAttributeValue(oUserXmlNode,
                "statuses_count/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, StatusesID);

            AppendInt32GraphMLAttributeValue(oUserXmlNode,
                "favourites_count/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, FavoritesID);

            AppendStringGraphMLAttributeValue(oUserXmlNode,
                "description/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, DescriptionID);

            AppendStringGraphMLAttributeValue(oUserXmlNode,
                "time_zone/text()", null, oGraphMLXmlDocument, oVertexXmlNode,
                TimeZoneID);

            AppendStringGraphMLAttributeValue(oUserXmlNode,
                "utc_offset/text()", null, oGraphMLXmlDocument, oVertexXmlNode,
                UtcOffsetID);

            String sJoinedDateUtc;

            if ( XmlUtil2.TrySelectSingleNodeAsString(oUserXmlNode,
                "created_at/text()", null, out sJoinedDateUtc) )
            {
                oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                    oVertexXmlNode, JoinedDateUtcID,
                    TwitterDateParser.ParseTwitterDate(sJoinedDateUtc) );
            }
            }

            String sLatestStatus;

            if ( XmlUtil2.TrySelectSingleNodeAsString(oUserXmlNode,
            "status/text/text()", null, out sLatestStatus) )
            {
            String sLatestStatusDateUtc;

            if ( XmlUtil2.TrySelectSingleNodeAsString(oUserXmlNode,
                "status/created_at/text()", null, out sLatestStatusDateUtc) )
            {
                sLatestStatusDateUtc = TwitterDateParser.ParseTwitterDate(
                    sLatestStatusDateUtc);
            }

            // Don't overwrite any status the derived class may have already
            // stored on the TwitterVertex object.

            if (oTwitterVertex.StatusForAnalysis == null)
            {
                oTwitterVertex.StatusForAnalysis = sLatestStatus;
                oTwitterVertex.StatusForAnalysisDateUtc = sLatestStatusDateUtc;
            }

            if (bIncludeLatestStatus)
            {
                oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                    LatestStatusID, sLatestStatus);

                if ( !String.IsNullOrEmpty(sLatestStatusDateUtc) )
                {
                    oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                        oVertexXmlNode, LatestStatusDateUtcID,
                        sLatestStatusDateUtc);
                }
            }
            }

            // Add an image URL GraphML-attribute if it hasn't already been added.
            // (It might have been added from an "entry" node by
            // TwitterSearchNetworkAnalyzer.)

            if (oVertexXmlNode.SelectSingleNode(
            "a:data[@key='" + ImageFileID + "']",
            oGraphMLXmlDocument.CreateXmlNamespaceManager("a") ) == null)
            {
            AppendStringGraphMLAttributeValue(oUserXmlNode,
                "profile_image_url/text()", null, oGraphMLXmlDocument,
                oVertexXmlNode, ImageFileID);
            }
        }
        //*************************************************************************
        //  Method: TryAppendVertexXmlNode()
        //
        /// <overloads>
        /// Appends a vertex XML node to the GraphML document for a person if such
        /// a node doesn't already exist.
        /// </overloads>
        ///
        /// <summary>
        /// Appends a vertex XML node to the GraphML document for a person if such
        /// a node doesn't already exist and provides the TwitterVertex for the
        /// node.
        /// </summary>
        ///
        /// <param name="sScreenName">
        /// Screen name to add a vertex XML node for.
        /// </param>
        ///
        /// <param name="oUserXmlNode">
        /// The "user" XML node returned by Twitter, or null if a user node isn't
        /// available.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oScreenNameDictionary">
        /// The key is the screen name in lower case and the value is the
        /// corresponding TwitterVertex.
        /// </param>
        ///
        /// <param name="bIncludeStatistics">
        /// true to include the user's statistics if <paramref
        /// name="oUserXmlNode" /> is not null.
        /// </param>
        ///
        /// <param name="bIncludeLatestStatus">
        /// true to include a latest status attribute value if <paramref
        /// name="oUserXmlNode" /> is not null.
        /// </param>
        ///
        /// <param name="oTwitterVertex">
        /// Where the TwitterVertex that wraps the vertex XML node gets stored.
        /// This gets set regardless of whether the node already existed.
        /// </param>
        ///
        /// <returns>
        /// true if a vertex XML node was added, false if a vertex XML node already
        /// existed.
        /// </returns>
        //*************************************************************************
        protected Boolean TryAppendVertexXmlNode(
            String sScreenName,
            XmlNode oUserXmlNode,
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, TwitterVertex> oScreenNameDictionary,
            Boolean bIncludeStatistics,
            Boolean bIncludeLatestStatus,
            out TwitterVertex oTwitterVertex
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sScreenName) );
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oScreenNameDictionary != null);

            oTwitterVertex = null;

            if ( oScreenNameDictionary.TryGetValue(sScreenName,
            out oTwitterVertex) )
            {
            return (false);
            }

            XmlNode oVertexXmlNode = oGraphMLXmlDocument.AppendVertexXmlNode(
            sScreenName);

            oTwitterVertex = new TwitterVertex(oVertexXmlNode);
            oScreenNameDictionary.Add(sScreenName, oTwitterVertex);

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
            MenuTextID, "Open Twitter Page for This Person");

            oGraphMLXmlDocument.AppendGraphMLAttributeValue( oVertexXmlNode,
            MenuActionID, String.Format(WebPageUrlPattern, sScreenName) );

            if (oUserXmlNode != null)
            {
            AppendFromUserXmlNode(oUserXmlNode, oGraphMLXmlDocument,
                oTwitterVertex, bIncludeStatistics, bIncludeLatestStatus);
            }

            return (true);
        }
        //*************************************************************************
        //  Method: GetUserNetworkRecursive()
        //
        /// <summary>
        /// Recursively gets a network of Flickr users.
        /// </summary>
        ///
        /// <param name="sUserID">
        /// Flickr user ID.
        /// </param>
        ///
        /// <param name="sScreenName">
        /// Flickr screen name.
        /// </param>
        ///
        /// <param name="eWhatToInclude">
        /// Specifies what should be included in the network.
        /// </param>
        ///
        /// <param name="bIncludeContactsThisCall">
        /// true to include the user's contacts, false to include the people who
        /// have commented on the user's photos.
        /// </param>
        ///
        /// <param name="eNetworkLevel">
        /// Network level to include.  Must be NetworkLevel.One, OnePointFive, or
        /// Two.
        /// </param>
        ///
        /// <param name="iMaximumPerRequest">
        /// Maximum number of people or photos to request for each query, or
        /// Int32.MaxValue for no limit.
        /// </param>
        ///
        /// <param name="sApiKey">
        /// Flickr API key.
        /// </param>
        ///
        /// <param name="iRecursionLevel">
        /// Recursion level for this call.  Must be 1 or 2.  Gets incremented when
        /// recursing.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="oUserIDDictionary">
        /// The key is the user ID and the value is the corresponding GraphML XML
        /// node that represents the user.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        //*************************************************************************
        protected void GetUserNetworkRecursive(
            String sUserID,
            String sScreenName,
            WhatToInclude eWhatToInclude,
            Boolean bIncludeContactsThisCall,
            NetworkLevel eNetworkLevel,
            Int32 iMaximumPerRequest,
            String sApiKey,
            Int32 iRecursionLevel,
            GraphMLXmlDocument oGraphMLXmlDocument,
            Dictionary<String, XmlNode> oUserIDDictionary,
            RequestStatistics oRequestStatistics
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sUserID) );
            Debug.Assert( !String.IsNullOrEmpty(sScreenName) );

            Debug.Assert(eNetworkLevel == NetworkLevel.One ||
            eNetworkLevel == NetworkLevel.OnePointFive ||
            eNetworkLevel == NetworkLevel.Two);

            Debug.Assert(iMaximumPerRequest > 0);
            Debug.Assert( !String.IsNullOrEmpty(sApiKey) );
            Debug.Assert(iRecursionLevel == 1 || iRecursionLevel == 2);
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert(oUserIDDictionary != null);
            Debug.Assert(oRequestStatistics != null);
            AssertValid();

            /*
            Here is what this method should do, based on the eNetworkLevel and
            iRecursionLevel parameters.

                eNetworkLevel

               |One               | OnePointFive      | Two
            ---|------------------| ------------------| -----------------
            i   1  |Add all vertices. | Add all vertices. | Add all vertices.
            R      |                  |                   |
            e      |Add all edges.    | Add all edges.    | Add all edges.
            c      |                  |                   |
            u      |Do not recurse.   | Recurse.          | Recurse.
            r      |                  |                   |
            s   ---|------------------|-------------------|------------------
            i   2  |Impossible.       | Do not add        | Add all vertices.
            o      |                  | vertices.         |
            n      |                  |                   |
            L      |                  | Add edges only if | Add all edges.
            e      |                  | vertices are      |
            v      |                  | already included. |
            e      |                  |                   |
            l      |                  | Do not recurse.   | Do not recurse.
               |                  |                   |
            ---|------------------|-------------------|------------------
            */

            Boolean bNeedToRecurse = GetNeedToRecurse(eNetworkLevel,
            iRecursionLevel);

            Boolean bNeedToAppendVertices = GetNeedToAppendVertices(eNetworkLevel,
            iRecursionLevel);

            List<String> oUserIDsToRecurse = new List<String>();

            ReportProgressForContactsOrCommenters(sScreenName,
            bIncludeContactsThisCall);

            Boolean bThisUserAppended = false;

            foreach ( XmlNode oChildXmlNode in GetContactsOrCommentersEnumerator(
            sUserID, bIncludeContactsThisCall, iMaximumPerRequest,
            oGraphMLXmlDocument, sApiKey, oRequestStatistics) )
            {
            String sOtherScreenName, sOtherUserID;

            if (
                !XmlUtil2.TrySelectSingleNodeAsString(oChildXmlNode,
                    bIncludeContactsThisCall ? "@username" : "@authorname",
                    null, out sOtherScreenName)
                ||
                !XmlUtil2.TrySelectSingleNodeAsString(oChildXmlNode,
                    bIncludeContactsThisCall ? "@nsid" : "@author",
                    null, out sOtherUserID)
                )
            {
                continue;
            }

            if (!bThisUserAppended)
            {
                // Append a vertex node for this request's user.
                //
                // This used to be done after the foreach loop, which avoided
                // the need for a "bThisUserAppended" flag.  That caused the
                // following bug: If a YouTube error occurred within
                // EnumerateXmlNodes() after some edges had been added, and the
                // user decided to import the resulting partial network, the
                // GraphML might contain edges that referenced "this user"
                // without containing a vertex for "this user."  That is an
                // illegal state for GraphML, which the ExcelTemplate project
                // caught and reported as an error.

                TryAppendVertexXmlNode(sUserID, sScreenName,
                    oGraphMLXmlDocument, oUserIDDictionary);

                bThisUserAppended = true;
            }

            if (bNeedToAppendVertices)
            {
                if (
                    TryAppendVertexXmlNode(sOtherUserID, sOtherScreenName,
                        oGraphMLXmlDocument, oUserIDDictionary)
                    &&
                    bNeedToRecurse
                    )
                {
                    oUserIDsToRecurse.Add(sOtherUserID);
                }
            }

            if ( bNeedToAppendVertices ||
                oUserIDDictionary.ContainsKey(sOtherUserID) )
            {
                // Append an edge node and optional attributes.

                XmlNode oEdgeXmlNode;

                if (bIncludeContactsThisCall)
                {
                    oEdgeXmlNode = AppendEdgeXmlNode(oGraphMLXmlDocument,
                        sScreenName, sOtherScreenName, "Contact");
                }
                else
                {
                    // (Note the swapping of screen names in the commenter
                    // case.)

                    oEdgeXmlNode = AppendEdgeXmlNode(oGraphMLXmlDocument,
                        sOtherScreenName, sScreenName, "Commenter");

                    UInt32 uCommentDateUtc;

                    if ( XmlUtil2.TrySelectSingleNodeAsUInt32(oChildXmlNode,
                            "@datecreate", null, out uCommentDateUtc) )
                    {
                        DateTime oCommentDateUtc =
                            DateTimeUtil2.UnixTimestampToDateTimeUtc(
                                uCommentDateUtc);

                        oGraphMLXmlDocument.AppendGraphMLAttributeValue(
                            oEdgeXmlNode, CommentDateUtcID,

                            ExcelDateTimeUtil.DateTimeToStringLocale1033(
                                oCommentDateUtc, ExcelColumnFormat.DateAndTime)
                            );
                    }

                    AppendStringGraphMLAttributeValue(oChildXmlNode,
                        "@permalink", null, oGraphMLXmlDocument, oEdgeXmlNode,
                        CommentUrlID);
                }
            }
            }

            if (bNeedToRecurse)
            {
            foreach (String sUserIDToRecurse in oUserIDsToRecurse)
            {
                XmlNode oVertexXmlNode = oUserIDDictionary[sUserIDToRecurse];

                String sScreenNameToRecurse = GetScreenNameFromVertexXmlNode(
                    oVertexXmlNode);

                GetUserNetworkRecursive(sUserIDToRecurse, sScreenNameToRecurse,
                    eWhatToInclude, bIncludeContactsThisCall, eNetworkLevel,
                    iMaximumPerRequest, sApiKey, 2, oGraphMLXmlDocument,
                    oUserIDDictionary, oRequestStatistics);
            }
            }
        }
        //*************************************************************************
        //  Method: AppendUserInformationGraphMLAttributeValues()
        //
        /// <summary>
        /// Appends user information GraphML attribute values to the GraphML
        /// document for one user in the network.
        /// </summary>
        ///
        /// <param name="sUserID">
        /// The user ID.
        /// </param>
        ///
        /// <param name="oVertexXmlNode">
        /// The GraphML XML node corresponding to the user.
        /// </param>
        ///
        /// <param name="oGraphMLXmlDocument">
        /// GraphMLXmlDocument being populated.
        /// </param>
        ///
        /// <param name="sApiKey">
        /// Flickr API key.
        /// </param>
        ///
        /// <param name="oRequestStatistics">
        /// A <see cref="RequestStatistics" /> object that is keeping track of
        /// requests made while getting the network.
        /// </param>
        //*************************************************************************
        protected void AppendUserInformationGraphMLAttributeValues(
            String sUserID,
            XmlNode oVertexXmlNode,
            GraphMLXmlDocument oGraphMLXmlDocument,
            String sApiKey,
            RequestStatistics oRequestStatistics
            )
        {
            Debug.Assert( !String.IsNullOrEmpty(sUserID) );
            Debug.Assert(oVertexXmlNode != null);
            Debug.Assert(oGraphMLXmlDocument != null);
            Debug.Assert( !String.IsNullOrEmpty(sApiKey) );
            Debug.Assert(oRequestStatistics != null);
            AssertValid();

            String sUrl = GetFlickrMethodUrl( "flickr.people.getInfo",
            sApiKey, GetUserIDUrlParameter(sUserID) );

            XmlDocument oXmlDocument;

            if ( !TryGetXmlDocument(sUrl, oRequestStatistics, out oXmlDocument) )
            {
            // Ignore errors.  The user information isn't critical.

            return;
            }

            const String XPathRoot = "rsp/person/";

            AppendStringGraphMLAttributeValue(oXmlDocument,
            XPathRoot + "realname/text()", null, oGraphMLXmlDocument,
            oVertexXmlNode, RealNameID);

            AppendInt32GraphMLAttributeValue(oXmlDocument,
            XPathRoot + "photos/count/text()", null, oGraphMLXmlDocument,
            oVertexXmlNode, TotalPhotosID);

            String sIsProfessional;

            if ( XmlUtil2.TrySelectSingleNodeAsString(oXmlDocument,
            XPathRoot + "@ispro", null, out sIsProfessional) )
            {
            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                IsProfessionalID,
                (sIsProfessional == "0") ? "No" : "Yes"
                );
            }

            // Get the URL to the user's buddy icon, which is explained here:
            //
            // http://www.flickr.com/services/api/misc.buddyicons.html

            Int32 iIconServer, iIconFarm;
            String sBuddyIconUrl;

            if (
            XmlUtil2.TrySelectSingleNodeAsInt32(oXmlDocument,
                XPathRoot + "@iconserver", null, out iIconServer)
            &&
            XmlUtil2.TrySelectSingleNodeAsInt32(oXmlDocument,
                XPathRoot + "@iconfarm", null, out iIconFarm)
            &&
            iIconServer > 0
            )
            {
            sBuddyIconUrl = String.Format(

                "http://farm{0}.static.flickr.com/{1}/buddyicons/{2}.jpg"
                ,
                iIconFarm,
                iIconServer,
                sUserID
                );
            }
            else
            {
            sBuddyIconUrl = "http://www.flickr.com/images/buddyicon.jpg";
            }

            oGraphMLXmlDocument.AppendGraphMLAttributeValue(
            oVertexXmlNode, ImageFileID, sBuddyIconUrl);

            if ( AppendStringGraphMLAttributeValue(oXmlDocument,
            XPathRoot + "photosurl/text()", null, oGraphMLXmlDocument,
            oVertexXmlNode, MenuActionID) )
            {
            oGraphMLXmlDocument.AppendGraphMLAttributeValue(oVertexXmlNode,
                MenuTextID, "Open Flickr Page for This Person");
            }
        }