//*************************************************************************
        //  Method: ReadRadius()
        //
        /// <summary>
        /// If a radius has been specified for a vertex, sets the vertex's radius.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oVertexRadiusConverter">
        /// Object that converts a vertex radius between values used in the Excel
        /// workbook and values used in the NodeXL graph.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the radius on.
        /// </param>
        ///
        /// <returns>
        /// If a radius has been specified for the vertex, the radius in workbook
        /// units is returned.  Otherwise, a Nullable that has no value is
        /// returned.
        /// </returns>
        //*************************************************************************
        protected Nullable<Single> ReadRadius(
            ExcelTableReader.ExcelTableRow oRow,
            VertexRadiusConverter oVertexRadiusConverter,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oVertex != null);
            Debug.Assert(oVertexRadiusConverter != null);
            AssertValid();

            String sRadius;

            if ( !oRow.TryGetNonEmptyStringFromCell(VertexTableColumnNames.Radius,
            out sRadius) )
            {
            return ( new Nullable<Single>() );
            }

            Single fRadius;

            if ( !Single.TryParse(sRadius, out fRadius) )
            {
            Range oInvalidCell = oRow.GetRangeForCell(
                VertexTableColumnNames.Radius);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an invalid size.  The vertex size,"
                + " which is optional, must be a number.  Any number is"
                + " acceptable, although {1} is used for any number less than"
                + " {1} and {2} is used for any number greater than {2}."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell),
                VertexRadiusConverter.MinimumRadiusWorkbook,
                VertexRadiusConverter.MaximumRadiusWorkbook
                ),

                oInvalidCell
            );
            }

            oVertex.SetValue( ReservedMetadataKeys.PerVertexRadius,
            oVertexRadiusConverter.WorkbookToGraph(fRadius) );

            return ( new Nullable<Single>(fRadius) );
        }
        //*************************************************************************
        //  Method: ReadLocked()
        //
        /// <summary>
        /// If a locked flag has been specified for a vertex, sets the vertex's
        /// locked flag.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oBooleanConverter">
        /// Object that converts a Boolean between values used in the Excel
        /// workbook and values used in the NodeXL graph.
        /// </param>
        ///
        /// <param name="bLocationSpecified">
        /// true if a location was specified for the vertex.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the lock flag on.
        /// </param>
        ///
        /// <remarks>
        /// "Locked" means "prevent the layout algorithm from moving the vertex."
        /// </remarks>
        //*************************************************************************
        protected void ReadLocked(
            ExcelTableReader.ExcelTableRow oRow,
            BooleanConverter oBooleanConverter,
            Boolean bLocationSpecified,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oBooleanConverter != null);
            Debug.Assert(oVertex != null);
            AssertValid();

            Boolean bLocked;

            if ( !TryGetBoolean(oRow, VertexTableColumnNames.Locked,
            oBooleanConverter, out bLocked) )
            {
            return;
            }

            if (bLocked && !bLocationSpecified)
            {
            Range oInvalidCell = oRow.GetRangeForCell(
                VertexTableColumnNames.Locked);

            OnWorkbookFormatError( String.Format(

                "The cell {0} indicates that the vertex should be locked,"
                + " but the vertex has no X and Y location values.  Either"
                + " clear the lock or specify a vertex location."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell)
                ),

                oInvalidCell
            );
            }

            oVertex.SetValue(ReservedMetadataKeys.LockVertexLocation, bLocked);
        }
        //*************************************************************************
        //  Method: ReadPolarCoordinates()
        //
        /// <summary>
        /// If polar coordinates have been specified for a vertex, sets the
        /// vertex's polar coordinates.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the polar coordinates on.
        /// </param>
        ///
        /// <returns>
        /// true if a location was specified.
        /// </returns>
        //*************************************************************************
        protected Boolean ReadPolarCoordinates(
            ExcelTableReader.ExcelTableRow oRow,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oVertex != null);
            AssertValid();

            String sR;

            Boolean bHasR = oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.PolarR, out sR);

            String sAngle;

            Boolean bHasAngle = oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.PolarAngle, out sAngle);

            if (bHasR != bHasAngle)
            {
            // R or Angle alone won't do.

            goto Error;
            }

            if (!bHasR && !bHasAngle)
            {
            return (false);
            }

            Single fR, fAngle;

            if ( !Single.TryParse(sR, out fR) ||
            !Single.TryParse(sAngle, out fAngle) )
            {
            goto Error;
            }

            oVertex.SetValue(ReservedMetadataKeys.PolarLayoutCoordinates,
            new SinglePolarCoordinates(fR, fAngle) );

            return (true);

            Error:

            Range oInvalidCell = oRow.GetRangeForCell(
                VertexTableColumnNames.PolarR);

            OnWorkbookFormatError( String.Format(

                "There is a problem with the vertex polar coordinates at {0}."
                + " If you enter polar coordinates, they must include both"
                + " {1} and {2} numbers.  Any numbers are acceptable."
                + "\r\n\r\n"
                + "Polar coordinates are used only when a Layout of Polar"
                + " or Polar Absolute is selected in the graph pane."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell),
                VertexTableColumnNames.PolarR,
                VertexTableColumnNames.PolarAngle
                ),

                oInvalidCell
                );

            // Make the compiler happy.

            return (false);
        }
        //*************************************************************************
        //  Method: ReadLayoutOrder()
        //
        /// <summary>
        /// If a layout order has been specified for a vertex, sets the vertex's
        /// layout order.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the layout order on.
        /// </param>
        ///
        /// <returns>
        /// true if a layout order was specified.
        /// </returns>
        //*************************************************************************
        protected Boolean ReadLayoutOrder(
            ExcelTableReader.ExcelTableRow oRow,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oVertex != null);
            AssertValid();

            String sOrder;

            if ( !oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.LayoutOrder, out sOrder) )
            {
            return (false);
            }

            Single fOrder;

            if ( !Single.TryParse(sOrder, out fOrder) )
            {
            Range oInvalidCell = oRow.GetRangeForCell(
                VertexTableColumnNames.LayoutOrder);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an invalid layout order.  The layout"
                + " order, which is optional, must be a number."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell)
                ),

                oInvalidCell
            );
            }

            oVertex.SetValue( ReservedMetadataKeys.SortableLayoutOrder, fOrder);

            return (true);
        }
        //*************************************************************************
        //  Method: ReadLocation()
        //
        /// <summary>
        /// If a location has been specified for a vertex, sets the vertex's
        /// location.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oVertexLocationConverter">
        /// Object that converts a vertex location between coordinates used in the
        /// Excel workbook and coordinates used in the NodeXL graph.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the location on.
        /// </param>
        ///
        /// <returns>
        /// true if a location was specified.
        /// </returns>
        //*************************************************************************
        protected Boolean ReadLocation(
            ExcelTableReader.ExcelTableRow oRow,
            VertexLocationConverter oVertexLocationConverter,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oVertexLocationConverter != null);
            Debug.Assert(oVertex != null);
            AssertValid();

            String sX;

            Boolean bHasX = oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.X, out sX);

            String sY;

            Boolean bHasY = oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.Y, out sY);

            if (bHasX != bHasY)
            {
            // X or Y alone won't do.

            goto Error;
            }

            if (!bHasX && !bHasY)
            {
            return (false);
            }

            Single fX, fY;

            if ( !Single.TryParse(sX, out fX) || !Single.TryParse(sY, out fY) )
            {
            goto Error;
            }

            // Transform the location from workbook coordinates to graph
            // coordinates.

            oVertex.Location = oVertexLocationConverter.WorkbookToGraph(fX, fY);

            return (true);

            Error:

            Range oInvalidCell = oRow.GetRangeForCell(
                VertexTableColumnNames.X);

            OnWorkbookFormatError( String.Format(

                "There is a problem with the vertex location at {0}.  If you"
                + " enter a vertex location, it must include both X and Y"
                + " numbers.  Any numbers are acceptable, although {1} is used"
                + " for any number less than {1} and and {2} is used for any"
                + " number greater than {2}."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell),

                VertexLocationConverter.MinimumXYWorkbook.ToString(
                    ExcelTemplateForm.Int32Format),

                VertexLocationConverter.MaximumXYWorkbook.ToString(
                    ExcelTemplateForm.Int32Format)
                ),

                oInvalidCell
                );

            // Make the compiler happy.

            return (false);
        }
        //*************************************************************************
        //  Method: ReadImageUri()
        //
        /// <summary>
        /// If an image URI has been specified for a vertex, sets the vertex's
        /// image.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to set the image on.
        /// </param>
        ///
        /// <param name="oVertexRadiusConverter">
        /// Object that converts a vertex radius between values used in the Excel
        /// workbook and values used in the NodeXL graph.
        /// </param>
        ///
        /// <param name="oVertexImageSize">
        /// The size to use for the image (in workbook units), or a Nullable that
        /// has no value to use the image's actual size.
        /// </param>
        ///
        /// <returns>
        /// true if an image key was specified.
        /// </returns>
        //*************************************************************************
        protected Boolean ReadImageUri(
            ExcelTableReader.ExcelTableRow oRow,
            IVertex oVertex,
            VertexRadiusConverter oVertexRadiusConverter,
            Nullable<Single> oVertexImageSize
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oVertex != null);
            Debug.Assert(oVertexRadiusConverter != null);
            AssertValid();

            String sImageUri;

            if ( !oRow.TryGetNonEmptyStringFromCell(
            VertexTableColumnNames.ImageUri, out sImageUri) )
            {
            return (false);
            }

            if ( sImageUri.ToLower().StartsWith("www.") )
            {
            // The Uri class thinks that "www.somewhere.com" is a relative
            // path.  Fix that.

            sImageUri= "http://" + sImageUri;
            }

            Uri oUri;

            // Is the URI either an URL or a full file path?

            if ( !Uri.TryCreate(sImageUri, UriKind.Absolute, out oUri) )
            {
            // No.  It appears to be a relative path.

            Range oCell = oRow.GetRangeForCell(
                VertexTableColumnNames.ImageUri);

            String sWorkbookPath =
                ( (Workbook)(oCell.Worksheet.Parent) ).Path;

            if ( !String.IsNullOrEmpty(sWorkbookPath) )
            {
                sImageUri = Path.Combine(sWorkbookPath, sImageUri);
            }
            else
            {
                OnWorkbookFormatError( String.Format(

                    "The image file path specified in cell {0} is a relative"
                    + " path.  Relative paths must be relative to the saved"
                    + " workbook file, but the workbook hasn't been saved yet."
                    + "  Either save the workbook or change the image file to"
                    + " an absolute path, such as \"C:\\MyImages\\Image.jpg\"."
                    ,
                    ExcelUtil.GetRangeAddress(oCell)
                    ),

                    oCell
                    );
            }
            }

            // Note that sImageUri may or may not be a valid URI string.  If it is
            // not, GetImageSynchronousIgnoreDpi() will return an error image.

            ImageSource oImage =
            ( new WpfImageUtil() ).GetImageSynchronousIgnoreDpi(sImageUri);

            if (oVertexImageSize.HasValue)
            {
            // Resize the image.

            Double dLongerDimension =
                oVertexRadiusConverter.WorkbookToLongerImageDimension(
                    oVertexImageSize.Value);

            Debug.Assert(dLongerDimension >= 1);

            oImage = ( new WpfImageUtil() ).ResizeImage(oImage,
                (Int32)dLongerDimension);
            }

            oVertex.SetValue(ReservedMetadataKeys.PerVertexImage, oImage);

            return (true);
        }
        //*************************************************************************
        //  Method: ReadCustomMenuItems()
        //
        /// <summary>
        /// If custom menu items have been specified for a vertex, stores the
        /// custom menu item information in the vertex.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the vertex data.
        /// </param>
        ///
        /// <param name="aoCustomMenuItemPairNames">
        /// Collection of pairs of column names, one element for each pair of
        /// columns that are used to add custom menu items to the vertex context
        /// menu in the graph.  They key is the name of the custom menu item text
        /// and the value is the name of the custom menu item action.
        /// </param>
        ///
        /// <param name="oVertex">
        /// Vertex to add custom menu item information to.
        /// </param>
        //*************************************************************************
        protected void ReadCustomMenuItems(
            ExcelTableReader.ExcelTableRow oRow,
            ICollection< KeyValuePair<String, String> > aoCustomMenuItemPairNames,
            IVertex oVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(aoCustomMenuItemPairNames != null);
            Debug.Assert(oVertex != null);
            AssertValid();

            // List of string pairs, one pair for each custom menu item to add to
            // the vertex's context menu in the graph.  The key is the custom menu
            // item text and the value is the custom menu item action.

            List<KeyValuePair<String, String>> oCustomMenuItemInformation =
            new List<KeyValuePair<String, String>>();

            foreach (KeyValuePair<String, String> oPairNames in
            aoCustomMenuItemPairNames)
            {
            String sCustomMenuItemText, sCustomMenuItemAction;

            // Both the menu item text and menu item action must be specified.
            // Skip the pair if either is missing.

            if (
                !oRow.TryGetNonEmptyStringFromCell(oPairNames.Key,
                    out sCustomMenuItemText)
                ||
                !oRow.TryGetNonEmptyStringFromCell(oPairNames.Value,
                    out sCustomMenuItemAction)
                )
            {
                continue;
            }

            Int32 iCustomMenuItemTextLength = sCustomMenuItemText.Length;

            if (iCustomMenuItemTextLength > MaximumCustomMenuItemTextLength)
            {
                Range oInvalidCell = oRow.GetRangeForCell(oPairNames.Key);

                OnWorkbookFormatError( String.Format(

                    "The cell {0} contains custom menu item text that is {1}"
                    + " characters long.  Custom menu item text can't be"
                    + " longer than {2} characters."
                    ,
                    ExcelUtil.GetRangeAddress(oInvalidCell),
                    iCustomMenuItemTextLength,
                    MaximumCustomMenuItemTextLength
                    ),

                    oInvalidCell
                    );
            }

            oCustomMenuItemInformation.Add( new KeyValuePair<String, String>(
                sCustomMenuItemText, sCustomMenuItemAction) );
            }

            if (oCustomMenuItemInformation.Count > 0)
            {
            oVertex.SetValue( ReservedMetadataKeys.CustomContextMenuItems,
                oCustomMenuItemInformation.ToArray() );
            }
        }
        //*************************************************************************
        //  Method: TryGetColor()
        //
        /// <summary>
        /// Attempts to get a color from a worksheet cell.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row to check.
        /// </param>
        ///
        /// <param name="sColumnName">
        /// Name of the column to check.
        /// </param>
        ///
        /// <param name="oColorConverter2">
        /// Object for converting the color from a string to a Color.
        /// </param>
        ///
        /// <param name="oColor">
        /// Where the color gets stored if true is returned.
        /// </param>
        ///
        /// <returns>
        /// true if the specified cell contains a valid color.
        /// </returns>
        ///
        /// <remarks>
        /// If the specified cell is empty, false is returned.  If the cell
        /// contains a valid color, the color gets stored at <paramref
        /// name="oColor" /> and true is returned.  If the cell contains an invalid
        /// color, a <see cref="WorkbookFormatException" /> is thrown.
        /// </remarks>
        //*************************************************************************
        protected Boolean TryGetColor(
            ExcelTableReader.ExcelTableRow oRow,
            String sColumnName,
            ColorConverter2 oColorConverter2,
            out Color oColor
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert( !String.IsNullOrEmpty(sColumnName) );
            Debug.Assert(oColorConverter2 != null);
            AssertValid();

            oColor = Color.Empty;

            String sColor;

            if ( !oRow.TryGetNonEmptyStringFromCell(sColumnName, out sColor) )
            {
            return (false);
            }

            if ( !oColorConverter2.TryWorkbookToGraph(sColor, out oColor) )
            {
            Range oInvalidCell = oRow.GetRangeForCell(sColumnName);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an unrecognized color.  Right-click the"
                + " cell and select Select Color on the right-click menu."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell)
                ),

                oInvalidCell
            );
            }

            return (true);
        }
        //*************************************************************************
        //  Method: ReadAlpha()
        //
        /// <summary>
        /// If an alpha has been specified for an edge or vertex, sets the alpha
        /// value on the edge or vertex.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the edge or vertex data.
        /// </param>
        ///
        /// <param name="oEdgeOrVertex">
        /// Edge or vertex to set the alpha on.
        /// </param>
        ///
        /// <returns>
        /// true if the edge or vertex was hidden.
        /// </returns>
        //*************************************************************************
        protected Boolean ReadAlpha(
            ExcelTableReader.ExcelTableRow oRow,
            IMetadataProvider oEdgeOrVertex
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert(oEdgeOrVertex != null);

            AssertValid();

            String sString;

            if ( !oRow.TryGetNonEmptyStringFromCell(CommonTableColumnNames.Alpha,
            out sString) )
            {
            return (false);
            }

            Single fAlpha;

            if ( !Single.TryParse(sString, out fAlpha) )
            {
            Range oInvalidCell = oRow.GetRangeForCell(
                CommonTableColumnNames.Alpha);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an invalid opacity.  The opacity,"
                + " which is optional, must be a number.  Any number is"
                + " acceptable, although {1} (transparent) is used for any"
                + " number less than {1} and {2} (opaque) is used for any"
                + " number greater than {2}."
                ,
                ExcelUtil.GetRangeAddress(oInvalidCell),
                AlphaConverter.MinimumAlphaWorkbook,
                AlphaConverter.MaximumAlphaWorkbook
                ),

                oInvalidCell
            );
            }

            fAlpha = m_oAlphaConverter.WorkbookToGraph(fAlpha);

            oEdgeOrVertex.SetValue(ReservedMetadataKeys.PerAlpha, fAlpha);

            return (fAlpha == 0);
        }
        //*************************************************************************
        //  Method: OnWorkbookFormatErrorWithDropDown()
        //
        /// <summary>
        /// Handles a workbook format error that prevents a graph from being
        /// created, where the invalid cell has a drop-down list.
        /// </summary>
        ///
        /// <param name="oRow">
        /// Row containing the invalid cell.
        /// </param>
        ///
        /// <param name="sColumnName">
        /// Name of the column containing the invalid cell.
        /// </param>
        ///
        /// <param name="sInvalidCellDescription">
        /// Description of the invalid cell.  Sample: "shape".
        /// </param>
        //*************************************************************************
        protected void OnWorkbookFormatErrorWithDropDown(
            ExcelTableReader.ExcelTableRow oRow,
            String sColumnName,
            String sInvalidCellDescription
            )
        {
            Debug.Assert(oRow != null);
            Debug.Assert( !String.IsNullOrEmpty(sColumnName) );
            Debug.Assert( !String.IsNullOrEmpty(sInvalidCellDescription) );
            AssertValid();

            Range oInvalidCell = oRow.GetRangeForCell(sColumnName);

            OnWorkbookFormatError( String.Format(

            "The cell {0} contains an invalid {1}.  Try selecting"
            + " from the cell's drop-down list instead."
            ,
            ExcelUtil.GetRangeAddress(oInvalidCell),
            sInvalidCellDescription
            ),

            oInvalidCell
            );
        }