/// <summary>
        /// Read a tile element. In this case, a tile element is any of "<Tile>",
        /// "<Outputter>", or "<Receiver>", where the string "type" parameter
        /// determines which of the three it is.
        /// </summary>
        private TileInfo ReadTileElement(XmlElement tile)
        {
            var tileName = tile.GetAttribute(TYPE_ATTRIBUTE);

            TileType tileType;
            var success = Enum.TryParse<TileType>(tileName, out tileType);
            if (!success)
            {
                Error_InvalidAttributeValue("tile type", tile.GetAttribute(TYPE_ATTRIBUTE));
            }

            // All non-empty tiles initially specified in the level file are closed to
            // prevent the player overwriting them.
            var tileInfo = new TileInfo(tileType, false);

            foreach (var attr in tile.Attributes.Cast<XmlAttribute>())
            {
                switch(attr.Name)
                {
                    case DIRECTION_ATTRIBUTE:
                        if (!Directions.DirectionsByName.ContainsKey(attr.InnerText))
                            Error_InvalidAttributeValue("direction", attr.InnerText);

                        var direction = Directions.DirectionsByName[attr.InnerText];
                        tileInfo.SetAttr<Directions>(attr.Name, direction);
                        break;
                    case COLOUR_ATTRIBUTE:
                        if (!LaserColours.ColoursByName.ContainsKey(attr.InnerText))
                            Error_InvalidAttributeValue("colour", attr.InnerText);

                        var colour = LaserColours.ColoursByName[attr.InnerText];
                        tileInfo.SetAttr<LaserColours>(attr.Name, colour);
                        break;
                    default:
                        break;
                }
            }

            // Check to ensure a tile of a specific type has the required
            // attributes.
            switch (tileType)
            {
                case TileType.Outputter:
                case TileType.Receiver:
                    if (!tileInfo.HasAttr<LaserColours>(COLOUR_ATTRIBUTE) ||
                        !tileInfo.HasAttr<Directions>(DIRECTION_ATTRIBUTE))
                        Error_NoColourOrDirectionAttribute(tileName.ToLower());
                    break;
                case TileType.OpenReceiver:
                case TileType.OpenReceiver_pass_L:
                case TileType.OpenReceiver_pass_U:
                case TileType.OpenReceiver_pass_UL:
                case TileType.OpenReceiver_pass_UR:
                    if (!tileInfo.HasAttr<LaserColours>(COLOUR_ATTRIBUTE))
                        Error_NoColourOrDirectionAttribute(tileName.ToLower());
                    break;
                default:
                    break;
            }

            return tileInfo;
        }