Source: widgets/chem/editor/kekule.chemEditor.operations.js

/**
 * @fileoverview
 * Operations need to implement an editor.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /utils/kekule.utils.js
 * requires /widgets/operation/kekule.operations.js
 * requires /core/kekule.structures.js
 * requires /widgets/chem/editor/kekule.chemEditor.baseEditors.js
 * requires /localization/
 */

(function(){
"use strict";

/**
 * A namespace for operation about normal ChemObject instance.
 * @namespace
 */
Kekule.ChemObjOperation = {};

/**
 * Base operation for ChemObject instance.
 * @class
 * @augments Kekule.Operation
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 *
 * @property {Kekule.ChemObject} target Target chem object.
 * @property {Bool} allowCoordBorrow Whether allow borrowing between 2D and 3D when manipulating coords.
 * @property {Kekule.Editor.BaseEditor} The editor object associated.
 */
Kekule.ChemObjOperation.Base = Class.create(Kekule.Operation,
/** @lends Kekule.ChemObjOperation.Base# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.Base',
	/** @constructs */
	initialize: function($super, chemObj, editor)
	{
		$super();
		this.setTarget(chemObj);
		if (editor)
			this.setEditor(editor);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('target', {'dataType': 'Kekule.ChemObject', 'serializable': false});
		this.defineProp('allowCoordBorrow', {'dataType': DataType.BOOL});
		this.defineProp('editor', {'dataType': 'Kekule.Editor.BaseEditor', 'serializable': false});
	},
	// A series of notification method to target object
	/** @private */
	notifyBeforeAddingByEditor: function(obj, parent, refSibling)
	{
		if (obj.beforeAddingByEditor)  // if this special notification method exists, call it first
			obj.beforeAddingByEditor(parent, refSibling);
	},
	/** @private */
	notifyBeforeRemovingByEditor: function(obj, parent)
	{
		if (obj.beforeRemovingByEditor)  // if this special notification method exists, call it first
			obj.beforeRemovingByEditor(parent);
	},
	/** @private */
	notifyBeforeModifyingByEditor: function(obj, propValues)
	{
		if (obj.beforeModifyingByEditor)  // if this special notification method exists, call it first
			obj.beforeModifyingByEditor(propValues);
	},
	/** @private */
	notifyAfterAddingByEditor: function(obj, parent, refSibling)
	{
		if (obj.afterAddingByEditor)  // if this special notification method exists, call it first
			obj.afterAddingByEditor(parent, refSibling);
	},
	/** @private */
	notifyAfterRemovingByEditor: function(obj, parent)
	{
		if (obj.afterRemovingByEditor)  // if this special notification method exists, call it first
			obj.afterRemovingByEditor(parent);
	},
	/** @private */
	notifyAfterModifyingByEditor: function(obj, propValues)
	{
		if (obj.afterModifyingByEditor)  // if this special notification method exists, call it first
			obj.afterModifyingByEditor(propValues);
	}
});

/**
 * Operation of changing a chemObject's properties.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Hash} newPropValues A hash of new prop-value map.
 *
 * @property {Hash} newPropValues A hash of new prop-value map.
 */
Kekule.ChemObjOperation.Modify = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemObjOperation.Modify# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.Modify',
	/** @constructs */
	initialize: function($super, chemObj, newPropValues, editor)
	{
		$super(chemObj, editor);
		if (newPropValues)
			this.setNewPropValues(newPropValues);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('newPropValues', {'dataType': DataType.HASH});
		this.defineProp('oldPropValues', {'dataType': DataType.HASH});  // private
	},
	/** @private */
	doExecute: function()
	{
		var oldValues = {};
		var map = this.getNewPropValues();
		var obj = this.getTarget();
		obj.beginUpdate();
		try
		{
			this.notifyBeforeModifyingByEditor(obj, map);
			for (var prop in map)
			{
				var value = map[prop];
				// store old value first
				oldValues[prop] = obj.getPropValue(prop);
				// set new value
				obj.setPropValue(prop, value);
			}
			this.notifyAfterModifyingByEditor(obj, map);
		}
		finally
		{
			obj.endUpdate();
		}
		this.setOldPropValues(oldValues);
	},
	/** @private */
	doReverse: function()
	{
		var map = this.getOldPropValues();
		var obj = this.getTarget();
		obj.beginUpdate();
		try
		{
			this.notifyBeforeModifyingByEditor(obj, map);
			for (var prop in map)
			{
				var value = map[prop];
				// restore old value
				obj.setPropValue(prop, value);
			}
			this.notifyAfterModifyingByEditor(obj, map);
		}
		finally
		{
			obj.endUpdate();
		}
	}
});

/**
 * Operation of changing a chemObject's coord.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Hash} newCoord
 * @param {Int} coordMode
 * @param {Bool} useAbsBaseCoord
 *
 * @property {Hash} newCoord
 * @property {Hash} oldCoord If old coord is not set, this property will be automatically calculated when execute the operation.
 * @property {Int} coordMode
 * @property {Bool} useAbsBaseCoord
 */
Kekule.ChemObjOperation.MoveTo = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemObjOperation.MoveTo# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.MoveTo',
	/** @constructs */
	initialize: function($super, chemObj, newCoord, coordMode, useAbsBaseCoord, editor)
	{
		$super(chemObj, editor);
		if (newCoord)
			this.setNewCoord(newCoord);
		this.setCoordMode(coordMode || Kekule.CoordMode.COORD2D);
		this.setUseAbsBaseCoord(!!useAbsBaseCoord);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('newCoord', {'dataType': DataType.HASH});
		this.defineProp('oldCoord', {'dataType': DataType.HASH});
		this.defineProp('coordMode', {'dataType': DataType.INT});
		this.defineProp('useAbsBaseCoord', {'dataType': DataType.BOOL});
	},
	/** @private */
	setObjCoord: function(obj, coord, coordMode)
	{
		if (obj && coord && coordMode)
		{
			var success = false;
			if (this.getUseAbsBaseCoord())
			{
				/*
				if (obj.setAbsCoordOfMode)
				{
					obj.setAbsCoordOfMode(coord, coordMode);
					success = true;
				}
				*/
				if (obj.setAbsBaseCoord)
				{
					obj.setAbsBaseCoord(coord, coordMode, this.getAllowCoordBorrow());
					success = true;
				}
			}
			else
			{
				if (obj.setCoordOfMode)
				{
					obj.setCoordOfMode(coord, coordMode);
					success = true;
				}
			}
			if (!success)
			{
				var className = obj.getClassName? obj.getClassName(): (typeof obj);
				Kekule.warn(/*Kekule.ErrorMsg.CAN_NOT_SET_COORD_OF_CLASS*/Kekule.$L('ErrorMsg.CAN_NOT_SET_COORD_OF_CLASS').format(className));
			}
		}
	},
	/** @private */
	getObjCoord: function(obj, coordMode)
	{
		if (this.getUseAbsBaseCoord())
		{
			/*
			if (obj.getAbsCoordOfMode)
				return obj.getAbsCoordOfMode(coordMode, this.getAllowCoordBorrow());
			*/
			if (obj.getAbsBaseCoord)
				return obj.getAbsBaseCoord(coordMode, this.getAllowCoordBorrow());
		}
		else
		{
			if (obj.getCoordOfMode)
				return obj.getCoordOfMode(coordMode, this.getAllowCoordBorrow());
		}

		return null;
	},
	/** @private */
	doExecute: function()
	{
		var obj = this.getTarget();
		if (!this.getOldCoord())
			this.setOldCoord(this.getObjCoord(obj, this.getCoordMode()));
		if (this.getNewCoord())
			this.setObjCoord(this.getTarget(), this.getNewCoord(), this.getCoordMode());
	},
	/** @private */
	doReverse: function()
	{
		if (this.getOldCoord())
		{
			this.setObjCoord(this.getTarget(), this.getOldCoord(), this.getCoordMode());
		}
	}
});

/**
 * Operation of changing a chem object's size and coord.
 * @class
 * @augments Kekule.ChemObjOperation.MoveTo
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Hash} newDimension {width, height}
 * @param {Hash} newCoord
 * @param {Int} coordMode
 * @param {Bool} useAbsCoord
 *
 * @property {Hash} newDimension
 * @property {Hash} oldDimension If old dimension is not set, this property will be automatically calculated when execute the operation.
 */
Kekule.ChemObjOperation.MoveAndResize = Class.create(Kekule.ChemObjOperation.MoveTo,
/** @lends Kekule.ChemObjOperation.MoveAndResize# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.MoveAndResize',
	/** @constructs */
	initialize: function($super, chemObj, newDimension, newCoord, coordMode, useAbsCoord, editor)
	{
		$super(chemObj, newCoord, coordMode, useAbsCoord, editor);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('newDimension', {'dataType': DataType.HASH});
		this.defineProp('oldDimension', {'dataType': DataType.HASH});
	},
	/** @private */
	setObjSize: function(obj, dimension, coordMode)
	{
		if (obj && dimension)
		{
			if (obj.setSizeOfMode)
			{
				obj.setSizeOfMode(dimension, coordMode);
			}
			else
			{
				var className = obj.getClassName? obj.getClassName(): (typeof obj);
				Kekule.warn(/*Kekule.ErrorMsg.CAN_NOT_SET_DIMENSION_OF_CLASS*/Kekule.$L('ErrorMsg.CAN_NOT_SET_DIMENSION_OF_CLASS').format(className));
			}
		}
	},
	/** @private */
	getObjSize: function(obj, coordMode)
	{
		if (obj.getSizeOfMode)
			return obj.getSizeOfMode(coordMode, this.getAllowCoordBorrow());
		else
			return null;
	},
	/** @private */
	doExecute: function($super)
	{
		$super();
		var obj = this.getTarget();
		if (!this.getOldDimension())
		{
			this.setOldDimension(this.getObjSize(obj, this.getCoordMode()));
		}
		if (this.getNewDimension())
			this.setObjSize(this.getTarget(), this.getNewDimension(), this.getCoordMode());
	},
	/** @private */
	doReverse: function($super)
	{
		if (this.getOldDimension())
			this.setObjSize(this.getTarget(), this.getOldDimension(), this.getCoordMode());
		$super();
	}
});

/**
 * Operation of adding a chem object to parent.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Kekule.ChemObject} parentObj Object should be added to.
 * @param {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling.
 *
 * @property {Kekule.ChemObject} parentObj Object should be added to.
 * @property {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling.
 */
Kekule.ChemObjOperation.Add = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemObjOperation.Add# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.Add',
	/** @constructs */
	initialize: function($super, chemObj, parentObj, refSibling, editor)
	{
		$super(chemObj, editor);
		this.setParentObj(parentObj);
		this.setRefSibling(refSibling);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('parentObj', {'dataType': 'Kekule.ChemObject', 'serializable': false});
		this.defineProp('refSibling', {'dataType': 'Kekule.ChemObject', 'serializable': false});
	},
	/** @private */
	doExecute: function()
	{
		var parent = this.getParentObj();
		var obj = this.getTarget();
		if (parent && obj)
		{
			var sibling = this.getRefSibling() || null;
			this.notifyBeforeAddingByEditor(obj, parent, sibling);
			parent.insertBefore(obj, sibling);
			this.notifyAfterAddingByEditor(obj, parent, sibling);
		}
	},
	/** @private */
	doReverse: function()
	{
		var obj = this.getTarget();
		/*
		var parent = obj.getParent? obj.getParent(): null;
		if (!parent)
			parent = this.getParentObj();
		if (parent !== this.getParentObj())
			console.log('[abnormal!!!!!!!]', parent.getId(), this.getParentObj().getId());
		*/
		var parent = this.getParentObj();
		if (parent && obj)
		{
			var sibling = this.getRefSibling();
			if (!sibling)  // auto calc
			{
				sibling = obj.getNextSibling();
				this.setRefSibling(sibling);
			}
			this.notifyBeforeRemovingByEditor(obj, parent);
			parent.removeChild(obj);
			this.notifyAfterRemovingByEditor(obj, parent);
		}
	}
});

/**
 * Operation of removing a chem object from its parent.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Kekule.ChemObject} parentObj Object should be added to.
 * @param {Kekule.ChemObject} refSibling Sibling after target object before removing.
 *
 * @property {Kekule.ChemObject} parentObj Object should be added to.
 * @property {Kekule.ChemObject} refSibling Sibling after target object before removing.
 *   This property is used in reversing the operation. If not set, it will be calculated automatically in execution.
 */
Kekule.ChemObjOperation.Remove = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemObjOperation.Remove# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemObjOperation.Remove',
	/** @constructs */
	initialize: function($super, chemObj, parentObj, refSibling, editor)
	{
		$super(chemObj, editor);
		this.setParentObj(parentObj);
		this.setRefSibling(refSibling);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('parentObj', {'dataType': 'Kekule.ChemObject', 'serializable': false});
		this.defineProp('ownerObj', {'dataType': 'Kekule.ChemObject', 'serializable': false});
		this.defineProp('refSibling', {'dataType': 'Kekule.ChemObject', 'serializable': false});
	},

	/** @private */
	_isInEditorSelection: function(obj)
	{
		var editor = this.getEditor();
		return ((editor && editor.getSelection && editor.getSelection()) || []).indexOf(obj) >= 0;
	},

	/** @private */
	doExecute: function()
	{
		var obj = this.getTarget();
		var parent = this.getParentObj();
		var owner = this.getOwnerObj();
		if (!parent && obj.getParent)
		{
			parent = obj.getParent();
			this.setParentObj(parent);
		}
		if (!owner && obj.getOwner)
		{
			owner = obj.getOwner();
			this.setOwnerObj(owner);
		}

		if (parent && obj)
		{
			if (!this.getRefSibling())
			{
				var sibling = obj.getNextSibling? obj.getNextSibling(): null;
				this.setRefSibling(sibling);
			}

			//console.log('remove', obj.getId());
			// ensure obj is also removed from editor's selection
			var editor = this.getEditor();
			var needModifySelection = this._isInEditorSelection(obj);
			if (needModifySelection)
				editor.beginUpdateSelection();

			//console.log('remove child', parent.getClassName(), obj.getClassName());
			this.notifyBeforeRemovingByEditor(obj, parent);
			parent.removeChild(obj);
			this.notifyAfterRemovingByEditor(obj, parent);

			if (needModifySelection)
			{
				//console.log('remove from selection', obj.getId());
				editor.removeFromSelection(obj);
				editor.endUpdateSelection();
			}
		}
	},
	/** @private */
	doReverse: function()
	{
		var parent = this.getParentObj();
		var owner = this.getOwnerObj();
		var obj = this.getTarget();
		if (parent && obj)
		{
			var sibling = this.getRefSibling();
			if (owner)
				obj.setOwner(owner);
			this.notifyBeforeAddingByEditor(obj, parent, sibling);
			parent.insertBefore(obj, sibling);
			this.notifyAfterAddingByEditor(obj, parent, sibling);
		}
	}
});

/**
 * A namespace for operation about Chem Structure instance.
 * @namespace
 */
Kekule.ChemStructOperation = {};

/**
 * Operation of adding a chem node to a structure fragment / molecule.
 * @class
 * @augments Kekule.ChemObjOperation.Add
 */
Kekule.ChemStructOperation.AddNode = Class.create(Kekule.ChemObjOperation.Add,
/** @lends Kekule.ChemStructOperation.AddNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.AddNode'
});

/**
 * Operation of removing a chem node from a structure fragment / molecule.
 * @class
 * @augments Kekule.ChemObjOperation.Remove
 *
 * @property {Array} linkedConnectors
 */
Kekule.ChemStructOperation.RemoveNode = Class.create(Kekule.ChemObjOperation.Remove,
/** @lends Kekule.ChemStructOperation.RemoveNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.RemoveNode',
	/** @private */
	initProperties: function()
	{
		this.defineProp('linkedConnectors', {'dataType': DataType.ARRAY, 'serializable': false});
	},
	/** @private */
	doExecute: function($super)
	{
		if (!this.getLinkedConnectors())
		{
			this.setLinkedConnectors(Kekule.ArrayUtils.clone(this.getTarget().getLinkedConnectors()));
		}
		$super()
	},
	/** @private */
	doReverse: function($super)
	{
		$super();
		var linkedConnectors = this.getLinkedConnectors();

		//console.log('reverse node', this.getTarget().getId());
		if (linkedConnectors && linkedConnectors.length)
		{
			//this.getTarget().setLinkedConnectors(linkedConnectors);
			var target = this.getTarget();
			//console.log('reverse append connector', linkedConnectors.length);
			for (var i = 0, l = linkedConnectors.length; i < l; ++i)
			{
				//linkedConnectors[i].appendConnectedObj(target);
				target.appendLinkedConnector(linkedConnectors[i]);
			}
		}
	}
});

/**
 * Operation of replace a chem node with another one.
 * @class
 * @augments Kekule.Operation
 */
Kekule.ChemStructOperation.ReplaceNode = Class.create(Kekule.Operation,
/** @lends Kekule.ChemStructOperation.ReplaceNode# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.ReplaceNode',
	/** @constructs */
	initialize: function($super, oldNode, newNode, parentObj, editor)
	{
		$super();
		this.setOldNode(oldNode);
		this.setNewNode(newNode);
		this.setParentObj(parentObj);
		this.setEditor(editor);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('oldNode', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false});
		this.defineProp('newNode', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false});
		this.defineProp('parentObj', {'dataType': 'Kekule.ChemStructureFragment', 'serializable': false});
		this.defineProp('editor', {'dataType': 'Kekule.Editor.BaseEditor', 'serializable': false});
	},
	/** @private */
	_isInEditorSelection: function(node)
	{
		var editor = this.getEditor();
		return ((editor && editor.getSelection && editor.getSelection()) || []).indexOf(node) >= 0;
	},
	/** @private */
	doExecute: function()
	{
		var oldNode = this.getOldNode();
		var newNode = this.getNewNode();
		if (oldNode && newNode)
		{
			var parent = this.getParentObj();
			if (!parent)
			{
				parent = oldNode.getParent();
				this.setParentObj(parent);
			}
			if (parent.replaceNode)
			{
				var editor = this.getEditor();
				var needModifySelection = this._isInEditorSelection(oldNode);
				if (needModifySelection)
					editor.beginUpdateSelection();
				parent.replaceNode(oldNode, newNode);
				if (needModifySelection)
				{
					editor.removeFromSelection(oldNode);
					editor.addObjToSelection(newNode);
					editor.endUpdateSelection();
				}
			}
		}
	},
	/** @private */
	doReverse: function()
	{
		var oldNode = this.getOldNode();
		var newNode = this.getNewNode();
		if (oldNode && newNode)
		{
			var parent = this.getParentObj() || newNode.getParent();
			if (parent.replaceNode)
			{
				//console.log('reverse!');
				var editor = this.getEditor();
				var needModifySelection = this._isInEditorSelection(newNode);
				if (needModifySelection)
					editor.beginUpdateSelection();
				parent.replaceNode(newNode, oldNode);
				if (needModifySelection)
				{
					editor.removeFromSelection(newNode)
					editor.addObjToSelection(oldNode);
					editor.endUpdateSelection();
				}
			}
		}
	}
});

/**
 * Operation of adding a chem connector to a structure fragment / molecule.
 * @class
 * @augments Kekule.ChemObjOperation.Add
 *
 * @param {Kekule.ChemObject} chemObject Target chem object.
 * @param {Kekule.ChemObject} parentObj Object should be added to.
 * @param {Kekule.ChemObject} refSibling If this property is set, chem object will be inserted before this sibling.
 * @param {Array} connectedObjs Objects that connected by this connector.
 *
 * @property {Array} connectedObjs Objects that connected by this connector.
 */
Kekule.ChemStructOperation.AddConnector = Class.create(Kekule.ChemObjOperation.Add,
/** @lends Kekule.ChemStructOperation.AddConnector# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.AddConnector',
	/** @constructs */
	initialize: function($super, chemObj, parentObj, refSibling, connectedObjs, editor)
	{
		$super(chemObj, parentObj, refSibling, editor);
		//this.setParentObj(parentObj);
		//this.setRefSibling(refSibling);
		this.setConnectedObjs(connectedObjs);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('connectedObjs', {'dataType': DataType.ARRAY, 'serializable': false});
	},
	/** @private */
	doExecute: function($super)
	{
		$super();
		var connObjs = Kekule.ArrayUtils.clone(this.getConnectedObjs());
		if (connObjs && connObjs.length)
		{
			this.getTarget().setConnectedObjs(connObjs);
		}
	},
	/** @private */
	doReverse: function($super)
	{
		$super();
	}
});

/**
 * Operation of removing a chem connector from a structure fragment / molecule.
 * @class
 * @augments Kekule.ChemObjOperation.Remove
 *
 * @property {Array} connectedObjs Objects that connected by this connector.
 *   This property is used in operation reversing. If not set, value will be automatically calculated in operation executing.
 */
Kekule.ChemStructOperation.RemoveConnector = Class.create(Kekule.ChemObjOperation.Remove,
/** @lends Kekule.ChemStructOperation.RemoveConnector# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.RemoveConnector',
	/** @private */
	initProperties: function()
	{
		this.defineProp('connectedObjs', {'dataType': DataType.ARRAY, 'serializable': false});
	},
	/** @private */
	doExecute: function($super)
	{
		if (!this.getConnectedObjs())
		{
			this.setConnectedObjs(Kekule.ArrayUtils.clone(this.getTarget().getConnectedObjs()));
		}
		$super()
	},
	/** @private */
	doReverse: function($super)
	{
		$super();
		var connObjs = this.getConnectedObjs();
		if (connObjs && connObjs.length)
		{
			this.getTarget().setConnectedObjs(connObjs);
		}
	}
});

/**
 * The base operation of merging two nodes as one, acts as the parent class of MergeNodes and MergeNodesPreview.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode.
 * @param {Kekule.ChemStructureNode} dest Destination node.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule.
 *
 * @property {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode.
 * @property {Kekule.ChemStructureNode} dest Destination node.
 * @property {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule.
 */
Kekule.ChemStructOperation.MergeNodesBase = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemStructOperation.MergeNodesBase# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeNodesBase',
	/** @constructs */
	initialize: function($super, target, dest, enableStructFragmentMerge, editor)
	{
		$super(target, editor);
		this.setDest(dest);
		this.setEnableStructFragmentMerge(enableStructFragmentMerge || false);
		this._refSibling = null;
		this._nodeParent = null;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('dest', {'dataType': 'Kekule.ChemStructureNode', 'serializable': false});
		this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL});
	},
	/**
	 * Returns nodes connected with both node1 and node2.
	 * @param {Kekule.ChemStructureNode} node1
	 * @param {Kekule.ChemStructureNode} node2
	 * @returns {Array}
	 */
	getCommonSiblings: function(node1, node2)
	{
		var siblings1 = node1.getLinkedObjs();
		var siblings2 = node2.getLinkedObjs();
		return Kekule.ArrayUtils.intersect(siblings1, siblings2);
	},
	/** @private */
	doExecute: function()
	{
		// do nothing, descendants should override
	},
	/** @private */
	doReverse: function()
	{
		// do nothing, descendants should override
	}
});

/**
 * Operation of merging two nodes as one.
 * @class
 * @augments Kekule.ChemStructOperation.MergeNodesBase
 *
 * @param {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode.
 * @param {Kekule.ChemStructureNode} dest Destination node.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule.
 *
 * @property {Array} changedConnectors Connectors modified during merge.
 * @property {Array} removedConnectors Connectors removed during merge.
 */
Kekule.ChemStructOperation.MergeNodes = Class.create(Kekule.ChemStructOperation.MergeNodesBase,
/** @lends Kekule.ChemStructOperation.MergeNodes# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeNodes',
	/** @constructs */
	initialize: function($super, target, dest, enableStructFragmentMerge, editor)
	{
		$super(target, dest, enableStructFragmentMerge, editor);
		this._refSibling = null;
		this._nodeParent = null;
		this._structFragmentMergeOperation = null;
		this._removeConnectorOperations = [];
		this._removeNodeOperation = null;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('changedConnectors', {'dataType': DataType.ARRAY, 'serializable': false});
		this.defineProp('removedConnectors', {'dataType': DataType.ARRAY, 'serializable': false});
		//this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL});
	},
	/** @ignore */
	doExecute: function()
	{
		var fromNode = this.getTarget();
		var toNode = this.getDest();
		var structFragment = fromNode.getParentFragment();
		var destFragment = toNode.getParentFragment();
		if (structFragment !== destFragment)  // from different molecule
		{
			//console.log('need merge mol');
			if (this.getEnableStructFragmentMerge())
			{
				this._structFragmentMergeOperation = new Kekule.ChemStructOperation.MergeStructFragment(structFragment, destFragment, this.getEditor());
				//this._structFragmentMergeOperation = new Kekule.ChemStructOperation.MergeStructFragment(destFragment, structFragment);
				this._structFragmentMergeOperation.execute();
				structFragment = destFragment;
			}
			else
				return null;
		}
		this._nodeParent = structFragment;

		structFragment.beginUpdate();
		try
		{
			var editor = this.getEditor();
			var removedConnectors = this.getRemovedConnectors();
			if (!removedConnectors)  // auto calc
			{
				var commonSiblings = this.getCommonSiblings(fromNode, toNode);
				var removedConnectors = [];
				if (commonSiblings.length)  // has common sibling between from/toNode, bypass bond between fromNode and sibling
				{
					for (var i = 0, l = commonSiblings.length; i < l; ++i)
					{
						var sibling = commonSiblings[i];
						var connector = fromNode.getConnectorTo(sibling);
						if (connector && (connector.getConnectedObjCount() == 2))
							removedConnectors.push(connector);
					}
				}
				var directConnector = fromNode.getConnectorTo(toNode);
				if (directConnector)
					removedConnectors.push(directConnector);
				this.setRemovedConnectors(removedConnectors);
			}

			var connectors = this.getChangedConnectors();
			if (!connectors)  // auto calc
			{
				var connectors = Kekule.ArrayUtils.clone(fromNode.getLinkedConnectors()) || [];
				connectors = Kekule.ArrayUtils.exclude(connectors, removedConnectors);
				this.setChangedConnectors(connectors);
			}

			// save fromNode's information
			this._refSibling = fromNode.getNextSibling();

			for (var i = 0, l = connectors.length; i < l; ++i)
			{
				var connector = connectors[i];
				var index = connector.indexOfConnectedObj(fromNode);
				connector.removeConnectedObj(fromNode);
				connector.insertConnectedObjAt(toNode, index);  // keep the index is important, wedge bond direction is related with node sequence
			}

			this._removeConnectorOperations = [];
			for (var i = 0, l = removedConnectors.length; i < l; ++i)
			{
				var connector = removedConnectors[i];
				var oper = new Kekule.ChemStructOperation.RemoveConnector(connector, null, null, editor);
				oper.execute();
				this._removeConnectorOperations.push(oper);
			}

			//structFragment.removeNode(fromNode);
			this._removeNodeOperation = new Kekule.ChemStructOperation.RemoveNode(fromNode, null, null, editor);
			this._removeNodeOperation.execute();
		}
		finally
		{
			structFragment.endUpdate();
		}
	},
	/** @ignore */
	doReverse: function()
	{
		var fromNode = this.getTarget();
		var toNode = this.getDest();
		//var structFragment = fromNode.getParent();
		//var structFragment = toNode.getParent();
		var structFragment = this._nodeParent;

		structFragment.beginUpdate();
		try
		{
			/*
			 console.log(fromNode.getParent(), fromNode.getParent() === structFragment,
			 toNode.getParent(), toNode.getParent() === structFragment);
			 */
			//structFragment.insertBefore(fromNode, this._refSibling);
			this._removeNodeOperation.reverse();

			if (this._removeConnectorOperations.length)
			{
				for (var i = this._removeConnectorOperations.length - 1; i >= 0; --i)
				{
					var oper = this._removeConnectorOperations[i];
					oper.reverse();
				}
			}
			this._removeConnectorOperations = [];

			var connectors = this.getChangedConnectors();

			//console.log('reverse node merge2', toNode, toNode.getParent());

			for (var i = 0, l = connectors.length; i < l; ++i)
			{
				var connector = connectors[i];
				var index = connector.indexOfConnectedObj(toNode);
				connector.removeConnectedObj(toNode);
				connector.insertConnectedObjAt(fromNode, index);
			}
		}
		finally
		{
			structFragment.endUpdate();
		}

		//console.log('reverse node merge', toNode, toNode.getParent());

		if (this._structFragmentMergeOperation)
		{
			this._structFragmentMergeOperation.reverse();
		}
	}
});
/**
 * A class method to check if two connectors can be merged
 * @param {Kekule.ChemStructureNode} target
 * @param {Kekule.ChemStructureNode} dest
 * @param {Bool} canMergeStructFragment
 * @returns {Bool}
 */
Kekule.ChemStructOperation.MergeNodes.canMerge = function(target, dest, canMergeStructFragment, canMergeNeighborNodes)
{
	// never allow merge to another molecule point (e.g. formula molecule) or subgroup
	if ((target instanceof Kekule.StructureFragment) || (dest instanceof Kekule.StructureFragment))
		return false;
	if (!((target instanceof Kekule.ChemStructureNode) && (dest instanceof Kekule.ChemStructureNode)))
		return false;
	var targetFragment = target.getParent();
	var destFragment = dest.getParent();
	var result = (targetFragment === destFragment) || canMergeStructFragment;
	if (!canMergeNeighborNodes)
		result = result && (!target.getConnectorTo(dest));
	return result;
};

/**
 * Preview operation of merging two nodes as one.
 * This operation just set the same position of merging nodes, but do not do the actual merge.
 * @class
 * @augments Kekule.ChemStructOperation.MergeNodesBase
 *
 * @param {Kekule.ChemStructureNode} target Source node, all connectors to this node will be connected to toNode.
 * @param {Kekule.ChemStructureNode} dest Destination node.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging nodes between different molecule.
 */
Kekule.ChemStructOperation.MergeNodesPreview = Class.create(Kekule.ChemStructOperation.MergeNodesBase,
/** @lends Kekule.ChemStructOperation.MergeNodesPreview# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeNodesPreview',
	/** @constructs */
	initialize: function($super, target, dest, enableStructFragmentMerge, editor)
	{
		$super(target, dest, enableStructFragmentMerge, editor);
		this._nodeParent = null;
	},
	/** @ignore */
	doExecute: function()
	{
		this._moveNodeOperations = [];
		var fromNode = this.getTarget();
		var toNode = this.getDest();
		var structFragment = fromNode.getParentFragment();
		/*
		if (!structFragment)
			console.log('merge from', fromNode.getId(), 'to', toNode.getId());
		*/
		var CM = Kekule.CoordMode;
		var coordModes = [CM.COORD2D, CM.COORD3D];
		if (structFragment)
			structFragment.beginUpdate();
		try
		{
			for (var i = 0, l = coordModes.length; i < l; ++i)
			{
				var toCoord = toNode.getAbsBaseCoord(coordModes[i], false);
				var oper = new Kekule.ChemObjOperation.MoveTo(fromNode, toCoord, coordModes[i], true, this.getEditor());
				oper.execute();
				this._moveNodeOperations.push(oper);
			}
			this._nodeParent = structFragment;
		}
		finally
		{
			if (structFragment)
				structFragment.endUpdate();
		}
	},
	/** @ignore */
	doReverse: function()
	{
		var structFragment = this._nodeParent;
		if (structFragment)
			structFragment.beginUpdate();
		try
		{
			var opers = this._moveNodeOperations;
			for (var i = opers.length - 1; i >= 0; --i)
			{
				opers[i].reverse();
			}
		}
		finally
		{
			if (structFragment)
				structFragment.endUpdate();
		}
		this._moveNodeOperations = null;
	}
});
Kekule.ChemStructOperation.MergeNodesPreview.canMerge = Kekule.ChemStructOperation.MergeNodes.canMerge;

/**
 * Operation of merging two connectors as one.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.ChemStructureConnector} target Source connector.
 * @param {Kekule.ChemStructureConnector} dest Destination connector.
 * @param {Int} coordMode Coord mode of current editor.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging connectors between different molecule.
 *
 * @property {Kekule.ChemStructureConnector} target Source connector.
 * @property {Kekule.ChemStructureConnector} dest Destination connector.
 * @property {Int} coordMode Coord mode of current editor.
 * @property {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging connectors between different molecule.
 */
Kekule.ChemStructOperation.MergeConnectorsBase = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemStructOperation.MergeConnectorsBase# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeConnectorsBase',
	/** @constructs */
	initialize: function($super, target, dest, coordMode, enableStructFragmentMerge, editor)
	{
		$super(target, editor);
		this.setDest(dest);
		this.setCoordMode(coordMode || Kekule.CoordMode.COORD2D);
		this.setEnableStructFragmentMerge(enableStructFragmentMerge || false);
		this._refSibling = null;
		this._nodeParent = null;
		//this._structFragmentMergeOperation = null;
		this._nodeMergeOperations = [];
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('dest', {'dataType': 'Kekule.ChemStructureConnector', 'serializable': false});
		this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL});
		this.defineProp('coordMode', {'dataType': DataType.INT});
	},
	/**
	 * Returns the concrete node merge operation, descendants should override this method.
	 */
	getMergeNodeOperationClass: function()
	{
		// do nothing here
	},
	/** @private */
	doExecute: function()
	{
		var canMerge = Kekule.ChemStructOperation.MergeConnectors.canMerge(this.getTarget(), this.getDest(), this.getEnableStructFragmentMerge());

		if (!canMerge)
			Kekule.error(Kekule.$L('ErrorMsg.CAN_NOT_MERGE_CONNECTORS'));

		// sort targetNodes and destNodes via coord
		var coordMode = this.getCoordMode();
		var connectors = [this.getTarget(), this.getDest()];
		var AU = Kekule.ArrayUtils;
		var targetNodes = AU.clone(this.getTarget().getConnectedObjs());
		var destNodes = AU.clone(this.getDest().getConnectedObjs());
		var allowCoordBorrow = this.getAllowCoordBorrow();

		for (var i = 0, l = connectors.length; i < l; ++i)
		{
			var connector = connectors[i];
			var coord1 = connector.getConnectedObjAt(0).getAbsCoordOfMode(coordMode, allowCoordBorrow);
			var coord2 = connector.getConnectedObjAt(1).getAbsCoordOfMode(coordMode, allowCoordBorrow);
			var coordDelta = Kekule.CoordUtils.substract(coord1, coord2);
			var dominateDirection = Math.abs(coordDelta.x) > Math.abs(coordDelta.y)? 'x': 'y';
			if (Kekule.ObjUtils.notUnset(coord1.z))
			{
				if (Math.abs(coordDelta.z) > Math.abs(coordDelta.x))
					dominateDirection = 'z';
			}
			var nodes = (i === 0)? targetNodes: destNodes;
			nodes.sort(function(a, b)
				{
					var coord1 = a.getAbsCoordOfMode(coordMode, allowCoordBorrow);
					var coord2 = b.getAbsCoordOfMode(coordMode, allowCoordBorrow);
					return (coord1[dominateDirection] - coord2[dominateDirection]) || 0;
				}
			);
		}

		var commonNodes = AU.intersect(targetNodes, destNodes);
		targetNodes = AU.exclude(targetNodes, commonNodes);
		destNodes = AU.exclude(destNodes, commonNodes);

		this._nodeMergeOperations = [];
		var nodeMergeOperClass = this.getMergeNodeOperationClass();
		for (var i = 0, l = targetNodes.length; i < l; ++i)
		{
			if (targetNodes[i] !== destNodes[i])
			{
				//var oper = new Kekule.ChemStructOperation.MergeNodes(targetNodes[i], destNodes[i], this.getEnableStructFragmentMerge());
				var oper = new nodeMergeOperClass(targetNodes[i], destNodes[i], this.getEnableStructFragmentMerge());
				oper.execute();
			}
			this._nodeMergeOperations.push(oper);
		}
	},
	/** @private */
	doReverse: function()
	{
		for (var i = this._nodeMergeOperations.length - 1; i >= 0; --i)
		{
			var oper = this._nodeMergeOperations[i];
			oper.reverse();
		}
		this._nodeMergeOperations = [];
	}
});
/**
 * Operation of merging two connectors as one.
 * @class
 * @augments Kekule.ChemStructOperation.MergeConnectorsBase
 *
 * @param {Kekule.ChemStructureConnector} target Source connector.
 * @param {Kekule.ChemStructureConnector} dest Destination connector.
 * @param {Int} coordMode Coord mode of current editor.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging connectors between different molecule.
 */
Kekule.ChemStructOperation.MergeConnectors = Class.create(Kekule.ChemStructOperation.MergeConnectorsBase,
/** @lends Kekule.ChemStructOperation.MergeConnectors# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeConnectors',
	/** @ignore */
	getMergeNodeOperationClass: function()
	{
		return Kekule.ChemStructOperation.MergeNodes;
	}
});
/**
 * A class method to check if two connectors can be merged
 * @param {Kekule.ChemStructureConnector} target
 * @param {Kekule.ChemStructureConnector} dest
 * @param {Bool} canMergeStructFragment
 * @returns {Bool}
 */
Kekule.ChemStructOperation.MergeConnectors.canMerge = function(target, dest, canMergeStructFragment)
{
	if (!canMergeStructFragment && (target.getParent() !== dest.getParent()))
		return false;
	if (target.isConnectingConnector() || dest.isConnectingConnector())
	{
		return false;
	}
	var targetNodes = target.getConnectedExposedObjs();
	var destNodes = dest.getConnectedObjs();
	if (targetNodes.length !== destNodes.length)
	{
		return false;
	}
	if (targetNodes.length !== 2)  // currently can only handle connector with 2 connected objects
	{
		return false;
	}
	if (Kekule.ArrayUtils.intersect(targetNodes, destNodes).length >= 1)
	{
		return false;
	}
	return true;
};
/**
 * Operation of merging two connectors as one.
 * @class
 * @augments Kekule.ChemStructOperation.MergeConnectorsBase
 *
 * @param {Kekule.ChemStructureConnector} target Source connector.
 * @param {Kekule.ChemStructureConnector} dest Destination connector.
 * @param {Int} coordMode Coord mode of current editor.
 * @param {Bool} enableStructFragmentMerge If true, molecule will be also merged when merging connectors between different molecule.
 */
Kekule.ChemStructOperation.MergeConnectorsPreview = Class.create(Kekule.ChemStructOperation.MergeConnectorsBase,
/** @lends Kekule.ChemStructOperation.MergeConnectors# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeConnectors',
	/** @ignore */
	getMergeNodeOperationClass: function()
	{
		return Kekule.ChemStructOperation.MergeNodesPreview;
	}
});
Kekule.ChemStructOperation.MergeConnectorsPreview.canMerge = Kekule.ChemStructOperation.MergeConnectors.canMerge;

/**
 * Operation of merging two structure fragment as one.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.StructureFragment} target Source fragment.
 * @param {Kekule.StructureFragment} dest Destination fragment.
 *
 * @property {Kekule.StructureFragment} target Source fragment, all connectors and nodes will be moved to dest fragment.
 * @property {Kekule.StructureFragment} dest Destination fragment.
 * @property {Array} mergedNodes Nodes moved from target to dest during merging.
 * @property {Array} mergedConnectors Connectors moved from target to dest during merging.
 */
Kekule.ChemStructOperation.MergeStructFragment = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemStructOperation.MergeStructFragment# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.MergeStructFragment',
	/** @constructs */
	initialize: function($super, target, dest, editor)
	{
		$super(target, editor);
		this.setDest(dest);
		this._removeOperation = null;
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('dest', {'dataType': 'Kekule.StructureFragment', 'serializable': false});
		this.defineProp('mergedNodes', {'dataType': DataType.ARRAY, 'serializable': false});
		this.defineProp('mergedConnectors', {'dataType': DataType.ARRAY, 'serializable': false});
	},
	/** @private */
	moveChildBetweenStructFragment: function(target, dest, nodes, connectors)
	{
		Kekule.ChemStructureUtils.moveChildBetweenStructFragment(target, dest, nodes, connectors);
	},
	/** @private */
	doExecute: function()
	{
		var target = this.getTarget();
		var dest = this.getDest();
		if (target && dest)
		{
			var nodes = Kekule.ArrayUtils.clone(target.getNodes());
			this.setMergedNodes(nodes);
			var connectors = Kekule.ArrayUtils.clone(target.getConnectors());
			this.setMergedConnectors(connectors);

			this.moveChildBetweenStructFragment(target, dest, nodes, connectors);
			var parent = target.getParent();
			if (parent)  // remove target from parent
			{
				this._removeOperation = new Kekule.ChemObjOperation.Remove(target, parent, null, this.getEditor());
				this._removeOperation.execute();
			}
		}
	},
	/** @private */
	doReverse: function()
	{
		var target = this.getTarget();
		var dest = this.getDest();
		if (target && dest)
		{
			if (this._removeOperation)
			{
				this._removeOperation.reverse();
				this._removeOperation = null;
			}
			var nodes = this.getMergedNodes();
			var connectors = this.getMergedConnectors();

			/*
			console.log('before mol merge reverse dest', dest.getNodeCount(), dest.getConnectorCount());
			console.log('before mol merge reverse target', target.getNodeCount(), target.getConnectorCount());

			console.log('reverse mol merge', nodes.length, connectors.length);
			*/
			this.moveChildBetweenStructFragment(dest, target, nodes, connectors);
			/*
			console.log('after mol merge reverse dest', dest.getNodeCount(), dest.getConnectorCount());
			console.log('after mol merge reverse target', target.getNodeCount(), target.getConnectorCount());
			*/
		}
	}
});

/**
 * Operation of split one unconnected structure fragment into multiple connected ones.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.StructureFragment} target.
 *
 * @property {Kekule.StructureFragment} target Source fragment, all connectors and nodes will be moved to dest fragment.
 * @property {Array} splittedFragments Fragment splitted, this property will be automatically calculated in execution of operation.
 */
Kekule.ChemStructOperation.SplitStructFragment = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemStructOperation.SplitStructFragment# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.SplitStructFragment',
	/** @constructs */
	initialize: function($super, target, editor)
	{
		$super(target, editor);
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('splittedFragments', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null});
	},
	/** @private */
	doExecute: function()
	{
		var splitted = Kekule.ChemStructureUtils.splitStructFragment(this.getTarget());
		if (splitted.length > 1)  // do really split
		{
			this.setPropStoreFieldValue('splittedFragments', splitted);
			var parent = this.getTarget().getParent();
			var ref = this.getTarget().getNextSibling();
			parent.beginUpdate();
			try
			{
				if (parent)  // insert newly splitted fragments
				{
					for (var i = 1, l = splitted.length; i < l; ++i)
					{
						var frag = splitted[i];
						parent.insertBefore(frag, ref);
					}
				}
			}
			finally
			{
				parent.endUpdate();
			}
		}
		else // no real split actions done
		{
			this.setPropStoreFieldValue('splittedFragments', null);
		}
	},
	/** @private */
	doReverse: function()
	{
		var fragments = this.getSplittedFragments();
		if (fragments && fragments.length)
		{
			var target = this.getTarget();
			for (var i = 0, l = fragments.length; i < l; ++i)
			{
				var frag = fragments[i];
				if (frag !== target)
				{
					Kekule.ChemStructureUtils.moveChildBetweenStructFragment(frag, target, Kekule.ArrayUtils.clone(frag.getNodes()), Kekule.ArrayUtils.clone(frag.getConnectors()));
					var p = frag.getParent();
					if (p)
						p.removeChild(frag);
					frag.finalize();
				}
			}
		}
		//console.log('split reverse done', target.getNodeCount(), target.getConnectorCount());
	}
});

/**
 * Split one unconnected structure fragment into multiple connected ones, or remove the fragment
 * if the fragment contains no node.
 * @class
 * @augments Kekule.ChemObjOperation.Base
 *
 * @param {Kekule.StructureFragment} target.
 *
 * @property {Kekule.StructureFragment} target Source fragment.
 * @property {Bool} enableRemove Whether allow remove empty structure fragment. Default is true.
 * @property {Bool} enableSplit Whether allow splitting structure fragment. Default is true.
 */
Kekule.ChemStructOperation.StandardizeStructFragment = Class.create(Kekule.ChemObjOperation.Base,
/** @lends Kekule.ChemStructOperation.StandardizeStructFragment# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemStructOperation.StandardizeStructFragment',
	/** @constructs */
	initialize: function($super, target, editor)
	{
		$super(target, editor);
		this.setEnableSplit(true);
		this.setEnableRemove(true);
		this._concreteOper = null;  // private
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('enableRemove', {'dataType': DataType.BOOL});
		this.defineProp('enableSplit', {'dataType': DataType.BOOL});
	},
	/** @private */
	doExecute: function()
	{
		var target = this.getTarget();
		var nodeCount = target.getNodeCount();
		this._concreteOper = null;
		var editor = this.getEditor();
		if (nodeCount <= 0)
		{
			if (this.getEnableRemove())
				this._concreteOper = new Kekule.ChemObjOperation.Remove(target, null, null, editor);
		}
		else
		{
			if (this.getEnableSplit())
				this._concreteOper = new Kekule.ChemStructOperation.SplitStructFragment(target, editor);
		}
		if (this._concreteOper)
			return this._concreteOper.execute();
		else
			return null;
	},
	/** @private */
	doReverse: function()
	{
		return this._concreteOper? this._concreteOper.reverse(): null;
	}
});

	})();