ReadLayoutAndZOrder
    (
        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.SortableLayoutAndZOrder,
            fOrder);

        return (true);
    }
    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);
    }
    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) );
    }
    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);
    }
    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);
    }
    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() );
        }
    }
    ReadLabelFontSize
    (
        ExcelTableReader.ExcelTableRow oRow,
        FontSizeConverter oFontSizeConverter,
        IEdge oEdge
    )
    {
        Debug.Assert(oRow != null);
        Debug.Assert(oFontSizeConverter != null);
        AssertValid();

        String sLabelFontSize;

        if ( !oRow.TryGetNonEmptyStringFromCell(
            EdgeTableColumnNames.LabelFontSize, out sLabelFontSize) )
        {
            return;
        }

        Single fLabelFontSize;

        if ( !Single.TryParse(sLabelFontSize, out fLabelFontSize) )
        {
            Range oInvalidCell = oRow.GetRangeForCell(
                EdgeTableColumnNames.LabelFontSize);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an invalid label font size.  The label"
                + " font 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),
                FontSizeConverter.MinimumFontSizeWorkbook,
                FontSizeConverter.MaximumFontSizeWorkbook
                ),

                oInvalidCell
            );
        }

        oEdge.SetValue(ReservedMetadataKeys.PerEdgeLabelFontSize, 
            oFontSizeConverter.WorkbookToGraph(fLabelFontSize) );
    }
    OnHalfEmptyEdgeRow
    (
        ExcelTableReader.ExcelTableRow oRow,
        Boolean bVertex1IsEmpty
    )
    {
        Debug.Assert(oRow != null);

        AssertValid();

        Range oVertex1Cell = oRow.GetRangeForCell(
            EdgeTableColumnNames.Vertex1Name);

        Range oVertex2Cell = oRow.GetRangeForCell(
            EdgeTableColumnNames.Vertex2Name);

        Range oEmptyCell = bVertex1IsEmpty ? oVertex1Cell : oVertex2Cell;
        Range oNonEmptyCell = bVertex1IsEmpty ? oVertex2Cell : oVertex1Cell;

        String sEmptyRangeAddress = ExcelUtil.GetRangeAddress(oEmptyCell);
        String sNonEmptyRangeAddress = ExcelUtil.GetRangeAddress(oNonEmptyCell);

        String sErrorMessage = String.Format(

            "Cell {0} contains a vertex name but cell {1} is empty."
            + "  You can include an empty row, which will be ignored,"
            + " but you can't include a half-empty row."
            + "\r\n\r\n"
            + "You can fix the problem by entering a vertex name in {1} or"
            + " deleting the name in {0}."
            ,
            sNonEmptyRangeAddress,
            sEmptyRangeAddress
            );

        OnWorkbookFormatError(sErrorMessage, oEmptyCell);
    }
    ReadWidth
    (
        ExcelTableReader.ExcelTableRow oRow,
        EdgeWidthConverter oEdgeWidthConverter,
        IEdge oEdge
    )
    {
        Debug.Assert(oRow != null);
        Debug.Assert(oEdgeWidthConverter != null);
        AssertValid();
                
        String sWidth;

        if ( !oRow.TryGetNonEmptyStringFromCell(EdgeTableColumnNames.Width,
            out sWidth) )
        {
            return;
        }

        Single fWidth;

        if ( !Single.TryParse(sWidth, out fWidth) )
        {
            Range oInvalidCell = oRow.GetRangeForCell(
                EdgeTableColumnNames.Width);

            OnWorkbookFormatError( String.Format(

                "The cell {0} contains an invalid width.  The edge width,"
                + " 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),
                EdgeWidthConverter.MinimumWidthWorkbook,
                EdgeWidthConverter.MaximumWidthWorkbook
                ),

                oInvalidCell
            );
        }

        oEdge.SetValue(ReservedMetadataKeys.PerEdgeWidth, 
            oEdgeWidthConverter.WorkbookToGraph(fWidth) );
    }
    TryGetLocation
    (
        ExcelTableReader.ExcelTableRow oRow,
        String sXColumnName,
        String sYColumnName,
        VertexLocationConverter oVertexLocationConverter,
        out PointF oLocation
    )
    {
        Debug.Assert(oRow != null);
        Debug.Assert( !String.IsNullOrEmpty(sXColumnName) );
        Debug.Assert( !String.IsNullOrEmpty(sYColumnName) );
        Debug.Assert(oVertexLocationConverter != null);
        AssertValid();

        oLocation = PointF.Empty;

        String sX, sY;

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

        Boolean bHasY = oRow.TryGetNonEmptyStringFromCell(
            sYColumnName, 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.

        oLocation = oVertexLocationConverter.WorkbookToGraph(fX, fY);

        return (true);

        Error:

            Range oInvalidCell = oRow.GetRangeForCell(sXColumnName);

            OnWorkbookFormatError( String.Format(

                "There is a problem with the location at {0}.  If you enter a"
                + " 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);
    }
    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);
    }
    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);
    }
    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
            );
    }