/**
* @fileoverview
* Implementation of glyphs defined by a series of nodes and paths.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /core/kekule.structures.js
* requires /chemdoc/kekule.glyph.base.js
*/
(function(){
"use strict";
/**
* Represent an node in glyph path.
* @class
* @augments Kekule.BaseStructureNode
* @param {String} id Id of this node.
* @param {String} nodeType Type of this glyph node. Value from {@link Kekule.Glyph.NodeType}.
* @param {Hash} coord2D The 2D coordinates of node, {x, y}, can be null.
* @param {Hash} coord3D The 3D coordinates of node, {x, y, z}, can be null.
*
* @property {String} nodeType Type of this glyph node.
*/
Kekule.Glyph.PathGlyphNode = Class.create(Kekule.BaseStructureNode,
/** @lends Kekule.Glyph.PathGlyphNode# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyphNode',
initialize: function($super, id, nodeType, coord2D, coord3D)
{
$super(id);
if (coord2D)
this.setCoord2D(coord2D);
if (coord3D)
this.setCoord3D(coord3D);
this.setNodeType(nodeType || Kekule.Glyph.NodeType.LOCATION);
},
/** @private */
initProperties: function()
{
this.defineProp('nodeType', {
'dataType': DataType.STRING,
'scope': Class.PropertyScope.PUBLIC
});
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setInteractMode(Kekule.ChemObjInteractMode.UNSELECTABLE);
}
});
/**
* Enumeration of path types.
* @class
*/
Kekule.Glyph.NodeType = {
/* A default node, same as location point, do not need to draw. */
/*
DEFAULT: 'default',
*/
/** Location point, do not need to draw. Default value of node type. */
LOCATION: 'location',
/** Control point, control the shape of glyph, do not need to draw. */
CONTROLLER: 'controller',
/** Do not need to draw and can not manipulate in editor. */
HIDDEN: 'hidden'
};
/**
* Represent control node of glyph path connector.
* @class
* @augments Kekule.BaseStructureNode
* @param {String} id Id of this node.
* @param {Hash} coord2D The 2D coordinates of node, {x, y}, can be null.
* @param {Hash} coord3D The 3D coordinates of node, {x, y, z}, can be null.
*
* @property {String} nodeType Type of this glyph node.
*/
Kekule.Glyph.PathGlyphConnectorControlNode = Class.create(Kekule.BaseStructureNode,
/** @lends Kekule.Glyph.PathGlyphConnectorControlNode# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyphConnectorControlNode',
initialize: function($super, id, coord2D, coord3D)
{
$super(id);
if (coord2D)
this.setCoord2D(coord2D);
if (coord3D)
this.setCoord3D(coord3D);
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setInteractMode(Kekule.ChemObjInteractMode.UNSELECTABLE);
},
/** @private */
getAutoIdPrefix: function()
{
return 'cn';
},
/** @private */
getParentConnector: function()
{
var p = this.getParent();
if (p && p instanceof Kekule.Glyph.PathGlyphConnector)
return p;
else
return null;
}
});
/**
* Enumeration of path types.
* @class
*/
Kekule.Glyph.PathType = {
/** A straight line, may contains arrow at beginning and ending. */
LINE: 'L',
/** A arc path */
ARC: 'A'
};
/**
* Enumeration of path end arrow types.
* @class
*/
Kekule.Glyph.ArrowType = {
NONE: null,
OPEN: 'open',
TRIANGLE: 'triangle'
};
/**
* Enumeration of arrow location around path.
* @class
*/
Kekule.Glyph.ArrowSide = {
BOTH: 0, // default
SINGLE: 1, // one one side of path
REVERSED: -1 // one side but at the different side of SINGLE
};
/**
* General connector between glyph nodes.
* @class
* @augments Kekule.BaseStructureConnector
* @param {String} id Id of this connector.
* @param {String} pathType Type of path to draw between connected nodes.
* @param {Array} connectedObjs Objects ({@link Kekule.ChemStructureObject}) connected by connected, usually a connector connects two nodes.
*
* @property {String} pathType Type of path to draw between connected nodes, value from {@link Kekule.Glyph.PathType}.
* @property {Hash} pathParams Other params to control the outlook of path. Mayb including the following fields:
* {
* lineCount: {Int} need to draw single or multiple line in path?
* lineGap: {Float} gap between multiple lines
* startArrowType:
* startArrowSide:
* startArrowLength, startArrowWidth:
* endArrowType:
* endArrowSide:
* endArrowLength, endArrowWidth:
* }
*/
Kekule.Glyph.PathGlyphConnector = Class.create(Kekule.BaseStructureConnector,
/** @lends Kekule.Glyph.PathGlyphConnector# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyphConnector',
/** @constructs */
initialize: function($super, id, pathType, connectedObjs)
{
$super(id, connectedObjs);
this.setPathType(pathType);
//this.setControlPoints([new Kekule.Glyph.PathGlyphConnectorControlNode(null, {x: 0.1, y: 0.1})]); // test
},
/** @private */
initProperties: function()
{
this.defineProp('pathType', {
'dataType': DataType.STRING,
'scope': Class.PropertyScope.PUBLISHED,
'enumSource': Kekule.Glyph.PathType
});
this.defineProp('pathParams', {
'dataType': DataType.HASH,
'scope': Class.PropertyScope.PUBLISHED,
'getter': function()
{
var result = this.getPropStoreFieldValue('pathParams');
if (!result)
{
result = {};
this.setPropStoreFieldValue('pathParams', result);
}
return result;
},
'setter': function(value)
{
if (!value)
this.setPropStoreFieldValue('pathParams', null);
else
this.setPropStoreFieldValue('pathParams', Object.extend({}, value, true));
}
});
this.defineProp('controlPoints', {
'dataType': DataType.ARRAY,
'scope': Class.PropertyScope.PUBLISHED,
'getter': function(autoCreate)
{
var result = this.getPropStoreFieldValue('controlPoints');
if (!result && autoCreate)
{
result = [];
this.setPropStoreFieldValue('controlPoints', result);
}
return result;
},
'setter': function(value)
{
this.clearControlPoints();
this.setPropStoreFieldValue('controlPoints', value);
this._updateControlPointsOwner();
this._updateControlPointsParent();
}
});
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setInteractMode(Kekule.ChemObjInteractMode.UNSELECTABLE);
},
// methods about coords
// since connector has child control points, it must implement related coord method for the children to get abs coord
/** @private */
getAbsCoord2D: function(allowCoordBorrow)
{
return this.getAbsCoordOfMode(Kekule.CoordMode.COORD2D, allowCoordBorrow);
},
/** @private */
getAbsCoord3D: function(allowCoordBorrow)
{
return this.getAbsCoordOfMode(Kekule.CoordMode.COORD2D, allowCoordBorrow);
},
/** @private */
getAbsCoordOfMode: function(coordMode, allowCoordBorrow)
{
var CU = Kekule.CoordUtils;
// coord is based on connected objects
var sum = {'x': 0, 'y': 0};
var count = 0;
for (var i = 0, l = this.getConnectedObjCount(); i < l; ++i)
{
var obj = this.getConnectedObjAt(i);
if (obj && obj.getAbsCoordOfMode)
{
var coord = obj.getAbsCoordOfMode(coordMode, allowCoordBorrow);
if (coord)
{
++count;
sum = CU.add(sum, coord);
}
}
}
return CU.divide(sum, count);
},
// methods about children
/**
* Remove childObj from connector.
* @param {Variant} childObj A child control point.
* @param {Bool} cascadeRemove Whether remove related objects (e.g., bond connected to an atom).
* @param {Bool} freeRemoved Whether free all removed objects.
*/
removeChildObj: function(childObj, cascadeRemove, freeRemoved)
{
var ps = this.getControlPoints();
if (ps)
{
var index = ps.indexOf(childObj);
if (index >= 0)
{
ps.splice(index, 1);
if (childObj.setOwner)
childObj.setOwner(null);
if (childObj.setParent)
childObj.setParent(null);
this.notifyPropSet('controlPoints', this.getControlPoints());
}
}
},
/**
* Remove child obj directly.
* @param {Variant} childObj A child node or connector.
*/
removeChild: function($super, obj)
{
return this.removeChildObj(obj) || $super(obj);
},
/**
* Check if childObj is a child node or connector of this fragment's ctab.
* @param {Kekule.ChemObject} childObj
* @returns {Bool}
*/
hasChildObj: function(childObj)
{
var ps = this.getControlPoints();
return ps && ps.indexOf(childObj) >= 0;
},
/**
* Returns next sibling node or connector to childObj.
* @param {Variant} childObj Node or connector.
* @returns {Variant}
*/
getNextSiblingOfChild: function(childObj)
{
var index = this.indexOfChild(childObj);
++index;
return this.getChildAt(index);
},
/**
* Get count of child objects.
* @returns {Int}
*/
getChildCount: function()
{
var ps = this.getControlPoints();
return (ps && ps.length) || 0;
},
/**
* Get child control point at index.
* @param {Int} index
* @returns {Variant}
*/
getChildAt: function(index)
{
var ps = this.getControlPoints() || [];
return ps[index];
},
/**
* Get the index of obj in children list.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOfChild: function(obj)
{
var ps = this.getControlPoints();
return ps? ps.indexOf(obj): -1;
},
/** @private */
_updateControlPointsOwner: function(owner)
{
if (!owner)
owner = this.getOwner();
var ps = this.getControlPoints();
if (ps)
{
for (var i = 0, l = ps.length; i < l; ++i)
{
var p = ps[i];
if (p.setOwner)
p.setOwner(owner);
}
}
},
/** @private */
_updateControlPointsParent: function(parent)
{
if (!parent)
parent = this;
var ps = this.getControlPoints();
if (ps)
{
for (var i = 0, l = ps.length; i < l; ++i)
{
var p = ps[i];
if (p.setParent)
p.setParent(parent);
}
}
},
/**
* Clear all control points of this connector.
* @private
*/
clearControlPoints: function()
{
var oldPoints = this.getControlPoints();
if (oldPoints)
oldPoints = Kekule.ArrayUtils.clone(oldPoints);
this.setPropStoreFieldValue('controlPoints', null);
if (oldPoints)
{
for (var i = 0, l = oldPoints.length; i < l; ++i)
{
var p = oldPoints[i];
if (p.setOwner)
p.setOwner(null);
if (p.setParent)
p.setParent(null);
}
}
this.notifyPropSet('controlPoints', null);
return this;
}
});
/**
* Control node of glyph arc path connector.
* @class
* @augments Kekule.Glyph.PathGlyphConnectorControlNode
*
* @property {String} nodeType Type of this glyph node.
*/
Kekule.Glyph.PathGlyphArcConnectorControlNode = Class.create(Kekule.Glyph.PathGlyphConnectorControlNode,
/** @lends Kekule.Glyph.PathGlyphArcConnectorControlNode# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyphArcConnectorControlNode',
/** @private */
initProperties: function()
{
this.defineProp('distanceToChord', {
'dataType': DataType.FLOAT
});
},
/** @ignore */
doGetCoord2D: function($super, allowCoordBorrow, allowCreateNew)
{
var CU = Kekule.CoordUtils;
// coord 2D is determinated by distance to chord
var d = this.getDistanceToChord();
if (Kekule.ObjUtils.notUnset(d))
{
var connector = this.getParentConnector();
if (connector)
{
var arcStartCoord = connector.getConnectedObjAt(0).getCoord2D();
var arcEndCoord = connector.getConnectedObjAt(1).getCoord2D();
if (arcStartCoord && arcEndCoord)
{
//var midCoord = CU.divide(CU.add(arcStartCoord, arcEndCoord), 2);
var chordVector = CU.substract(arcEndCoord, arcStartCoord);
var signX = Math.sign(chordVector.x);
var signY = Math.sign(chordVector.y);
if (Kekule.NumUtils.isFloatEqual(chordVector.x, 0, 1e-10)) // vertical line
{
result = {'x': - d * signY, 'y': 0}
}
else if (Kekule.NumUtils.isFloatEqual(chordVector.y, 0, 1e-10)) // horizontal line
{
result = {'x': 0, 'y': d * signX};
}
else
{
var chordSlope = chordVector.y / chordVector.x;
var refSlope = -1 / chordSlope;
var result = {'x': -signY * d / Math.sqrt(1 + Math.sqr(refSlope))};
result.y = result.x * refSlope;
}
//console.log(d, arcStartCoord, arcEndCoord);
return result;
}
}
}
return $super(allowCoordBorrow, allowCreateNew);
},
/** @ignore */
doSetCoord2D: function($super, value)
{
var CU = Kekule.CoordUtils;
// the control point of arc should alway be at the middle of arc, so do this constraint
var oldCoord = this.getCoord2D();
var connector = this.getParentConnector();
if (connector)
{
var arcStartCoord = connector.getConnectedObjAt(0).getCoord2D();
var arcEndCoord = connector.getConnectedObjAt(1).getCoord2D();
var baseVector = CU.substract(arcEndCoord, arcStartCoord);
var baseAngle = Math.atan2(baseVector.y, baseVector.x);
var valueDeltaVector = CU.substract(value, oldCoord);
var valueDeltaAngle = Math.atan2(valueDeltaVector.y, valueDeltaVector.x);
var valueDeltaLength = CU.getDistance(value, oldCoord);
var actualMovement = valueDeltaLength * Math.sin(valueDeltaAngle - baseAngle);
var newDistanceToChord = (this.getDistanceToChord() || 0) + actualMovement;
this.setDistanceToChord(newDistanceToChord);
/*
var angle = Math.PI / 2 - baseAngle;
var actualDelta = {'x': actualMovement * Math.cos(angle), 'y': actualMovement * Math.sin(angle)};
var newCoord = CU.add(oldCoord, actualDelta);
return $super(newCoord);
*/
return;
}
return $super(value);
}
});
/**
* Arc shaped connector between glyph nodes.
* @class
* @augments Kekule.Glyph.PathGlyphConnector
* @param {String} id Id of this connector.
* @param {Array} connectedObjs Objects ({@link Kekule.ChemStructureObject}) connected by connected, usually a connector connects two nodes.
*/
Kekule.Glyph.PathGlyphArcConnector = Class.create(Kekule.Glyph.PathGlyphConnector,
/** @lends Kekule.Glyph.PathGlyphArcConnector# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyphArcConnector',
/** @constructs */
initialize: function($super, id, connectedObjs)
{
$super(id, Kekule.Glyph.PathType.ARC, connectedObjs);
// add control point to control the arc
this.setControlPoints([new Kekule.Glyph.PathGlyphArcConnectorControlNode(null, {x: 0, y: 0})]);
},
/**
* Returns the arc control point.
* @returns {Kekule.Glyph.PathGlyphArcConnectorControlNode}
*/
getControlPoint: function()
{
return (this.getControlPoints() || [])[0]
}
});
/**
* A glyph defined by a series of nodes and connectors (paths).
* @class
* @augments Kekule.Glyph
* @param {String} id Id of this node.
* @param {Float} refLength ref length of editor, this value will be used to create suitable connector length.
* @param {Hash} initialParams InitialParams used for creating connector and nodes.
* Can including all the fields in pathParams property of connector.
* Note: in initialParams, length fields(e.g. startArrowLength, endArrowLength) are based on refLength,
* field * refLength will be the actual length passed to connector. In private method createDefaultStructure,
* those length fields will be converted into actual length and passed into doCreateDefaultStructure.
* @param {Object} coord2D The 2D coordinates of node, {x, y}, can be null.
* @param {Object} coord3D The 3D coordinates of node, {x, y, z}, can be null.
*
* @property {Array} nodes All nodes in this glyph.
* @property {Array} connectors Connectors (paths) in this glyph.
*/
Kekule.Glyph.PathGlyph = Class.create(Kekule.Glyph.Base,
/** @lends Kekule.Glyph.PathGlyph# */
{
/** @private */
CLASS_NAME: 'Kekule.Glyph.PathGlyph',
/**
* @constructs
*/
initialize: function($super, id, refLength, initialParams, coord2D, coord3D)
{
$super(id, coord2D, coord3D);
this.createDefaultStructure(refLength || 1, initialParams || {});
},
doFinalize: function($super)
{
if (this.hasCtab())
this.getCtab().finalize();
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('ctab', {
'dataType': 'Kekule.StructureConnectionTable',
'scope': Class.PropertyScope.PUBLIC,
'getter': function(allowCreate)
{
if (!this.getPropStoreFieldValue('ctab'))
{
if (allowCreate)
this.createCtab();
}
return this.getPropStoreFieldValue('ctab');
},
'setter': function(value)
{
var old = this.getPropStoreFieldValue('ctab');
if (old)
{
old.finalize();
old = null;
}
if (value)
{
value.setPropValue('parent', this, true);
value.setOwner(this.getOwner());
}
this.setPropStoreFieldValue('ctab', value);
}
});
// values are read from ctab
this.defineProp('nodes', {
'dataType': DataType.ARRAY,
'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function() { return this.hasCtab()? this.getCtab().getNodes(): []; }
});
this.defineProp('connectors', {
'dataType': DataType.ARRAY,
'serializable': false,
'scope': Class.PropertyScope.PUBLIC,
'setter': null,
'getter': function() { return this.hasCtab()? this.getCtab().getConnectors(): []; }
});
},
/**
* Create default structure by ref length.
* @param {Float} refLength
* @param {Hash} initialParams
* @private
*/
createDefaultStructure: function(refLength, initialParams)
{
if (!refLength)
refLength = 1;
var actualParams = {};
var lengthFields = ['lineGap', 'startArrowLength', 'startArrowWidth', 'endArrowLength', 'endArrowWidth'];
for (var field in initialParams)
{
if (lengthFields.indexOf(field) >= 0)
{
actualParams[field] = initialParams[field] * refLength;
//console.log('transform', field, refLength, initialParams[field], actualParams[field]);
}
else
actualParams[field] = initialParams[field];
}
return this.doCreateDefaultStructure(refLength, actualParams);
},
/**
* Do actual work of createDefaultStructure.
* Descendants need to override this method.
* @param {Float} refLength
* @param {Hash} initialParams
* @private
*/
doCreateDefaultStructure: function(refLength, initialParams)
{
// do nothing here
},
/** @private */
getAutoIdPrefix: function()
{
return 'p';
},
/** @private */
ownerChanged: function($super, newOwner)
{
if (this.hasCtab())
this.getCtab().setOwner(newOwner);
$super(newOwner);
},
/** @private */
_removeChildObj: function(obj)
{
if (this.hasCtab())
{
var ctab = this.getCtab();
if (ctab === obj)
this.removeCtab();
else
{
if (ctab.hasChildObj(obj))
ctab.removeChildObj(obj);
}
}
},
/**
* Returns if this fragment has no formula or ctab, or ctab has no nodes or connectors.
* @return {Bool}
*/
isEmpty: function()
{
return this.getCtab().isEmpty();
},
/** @private */
createCtab: function()
{
var ctab = new Kekule.StructureConnectionTable(this.getOwner(), this);
this.setPropStoreFieldValue('ctab', ctab);
// install event listeners to ctab
ctab.addEventListener('propValueSet',
function(e)
{
if (e.propName == 'nodes')
{
this.notifyPropSet(e.propName, e.propValue);
}
}, this);
ctab.setEnablePropValueSetEvent(true); // to enable propValueSet event
},
/**
* Check whether a connection table is used to represent this fragment.
*/
hasCtab: function()
{
return (!!this.getPropStoreFieldValue('ctab'));
},
/**
* Calculate the box to fit whole glyph.
* @param {Int} coordMode Determine to calculate 2D or 3D box. Value from {@link Kekule.CoordMode}.
* @param {Bool} allowCoordBorrow
* @returns {Hash} Box information. {x1, y1, z1, x2, y2, z2} (in 2D mode z1 and z2 will not be set).
*/
getContainerBox: function($super, coordMode, allowCoordBorrow)
{
if (this.hasCtab())
{
return this.getCtab().getContainerBox(coordMode, allowCoordBorrow);
}
else
return $super(coordMode);
},
/**
* Get a structure node object with a specified id.
* @param {String} id
* @returns {Kekule.ChemStructureNode}
*/
getNodeById: function(id)
{
/*
var nodes = this.getNodes();
for (var i = 0, l = nodes.length; i < l; ++i)
{
if (nodes[i].getId() == id)
return nodes[i];
}
return null;
*/
return this.hasCtab()? this.getCtab().getNodeById(id): null;
},
/**
* Get a structure connector object with a specified id.
* @param {String} id
* @returns {Kekule.ChemStructureConnector}
*/
getConnectorById: function(id)
{
/*
var connectors = this.getConnectors();
for (var i = 0, l = connectors.length; i < l; ++i)
{
if (connectors[i].getId() == id)
return connectors[i];
}
return null;
*/
return this.hasCtab()? this.getCtab().getConnectorById(id): null;
},
/**
* Get a structure node or connector object with a specified id.
* @param {String} id
* @returns {Kekule.ChemStructureObject}
*/
getObjectById: function(id)
{
var node = this.getNodeById(id);
return node? node: this.getConnectorById(id);
},
/**
* Return count of nodes.
* @returns {Int}
*/
getNodeCount: function()
{
return this.hasCtab()? this.getCtab().getNodeCount(): 0;
},
/**
* Get node at index.
* @param {Int} index
* @returns {Kekule.ChemStructureNode}
*/
getNodeAt: function(index)
{
return this.hasCtab()? this.getCtab().getNodeAt(index): null;
},
/**
* Get index of node.
* @param {Kekule.ChemStructureNode} node
* @returns {Int}
*/
indexOfNode: function(node)
{
return this.hasCtab()? this.getCtab().indexOfNode(node): -1;
},
/**
* Check if a node exists in structure.
* @param {Kekule.ChemStructureNode} node Node to seek.
* @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
* @returns {Bool}
*/
hasNode: function(node, checkNestedStructure)
{
return this.hasCtab()? the.getCtab().hasNode(node, checkNestedStructure): null;
},
/**
* Add node to container. If node already in container, nothing will be done.
* @param {Kekule.ChemStructureNode} node
*/
appendNode: function(node)
{
/*
if (this.getNodes().indexOf(node) >= 0) // already exists
;// do nothing
else
{
var result = this.getNodes().push(node);
this.notifyNodesChanged();
return result;
}
*/
return this.doGetCtab(true).appendNode(node);
},
/**
* Insert node to index. If index is not set, node will be inserted as the first node of ctab.
* @param {Kekule.ChemStructureNode} node
* @param {Int} index
*/
insertNodeAt: function(node, index)
{
return this.doGetCtab(true).insertNodeAt(node, index);
},
/**
* Remove node at index in container.
* @param {Int} index
* @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
*/
removeNodeAt: function(index, preserveLinkedConnectors)
{
/*
var node = this.getNodes()[index];
if (node)
{
// remove from connectors
this.removeConnectNode(node);
this.getNodes().removeAt(index);
this.notifyNodesChanged();
}
*/
if (!this.hasCtab())
return null;
return this.getCtab().removeNodeAt(index, preserveLinkedConnectors);
},
/**
* Remove a node in container.
* @param {Kekule.ChemStructureNode} node
* @param {Bool} preserveLinkedConnectors Whether remove relations between this node and linked connectors.
*/
removeNode: function(node, preserveLinkedConnectors)
{
/*
var index = this.getNodes().indexOf(node);
if (index >= 0)
this.removeNodeAt(index);
*/
if (!this.hasCtab())
return null;
return this.getCtab().removeNode(node, preserveLinkedConnectors);
},
/**
* Replace oldNode with new one, preserve coords and all linked connectors.
* @param {Kekule.ChemStructureNode} oldNode Must be direct child of current fragment (node in nested structure fragment will be ignored).
* @param {Kekule.ChemStructureNode} newNode
*/
replaceNode: function(oldNode, newNode)
{
if (!this.hasCtab())
return null;
return this.getCtab().replaceNode(oldNode, newNode);
},
/**
* Remove all nodes.
*/
clearNodes: function()
{
if (this.hasCtab())
return this.getCtab().clearNodes();
},
/**
* Check if child nodes has 2D coord.
* @param {Bool} allowCoordBorrow
* @returns {Bool}
*/
nodesHasCoord2D: function(allowCoordBorrow)
{
if (!this.hasCtab())
return false;
return this.getCtab().nodesHasCoord2D(allowCoordBorrow);
},
/**
* Check if child nodes has 3D coord.
* @param {Bool} allowCoordBorrow
* @returns {Bool}
*/
nodesHasCoord3D: function(allowCoordBorrow)
{
if (!this.hasCtab())
return false;
return this.getCtab().nodesHasCoord3D(allowCoordBorrow);
},
/**
* Return count of connectors.
* @returns {Int}
*/
getConnectorCount: function()
{
return this.hasCtab()? this.getCtab().getConnectorCount(): 0;
},
/**
* Get connector at index.
* @param {Int} index
* @returns {Kekule.ChemStructureConnector}
*/
getConnectorAt: function(index)
{
//return this.getConnectors()[index];
return this.hasCtab()? this.getCtab().getConnectorAt(index): null;
},
/**
* Get index of connector inside fragment.
* @param {Kekule.ChemStructureConnector} connector
* @returns {Int}
*/
indexOfConnector: function(connector)
{
return this.hasCtab()? this.getCtab().indexOfConnector(connector): -1;
},
/**
* Check if a connector exists in structure.
* @param {Kekule.ChemStructureConnector} connector Connector to seek.
* @param {Bool} checkNestedStructure If true the nested sub groups will also be checked.
* @returns {Bool}
*/
hasConnector: function(connector, checkNestedStructure)
{
return this.hasCtab()? this.getCtab().hasConnector(connector, checkNestedStructure): null;
},
/**
* Add connector to container.
* @param {Kekule.ChemStructureConnector} connector
*/
appendConnector: function(connector)
{
/*
if (this.getConnectors().indexOf(connector) >= 0) // already exists
;// do nothing
else
{
return this.getConnectors().push(connector);
this.notifyConnectorsChanged();
}
*/
return this.doGetCtab(true).appendConnector(connector);
},
/**
* Insert connector to index. If index is not set, node will be inserted as the first connector of ctab.
* @param {Kekule.ChemStructureConnector} connector
* @param {Int} index
*/
insertConnectorAt: function(connector, index)
{
return this.doGetCtab(true).insertConnectorAt(connector, index);
},
/**
* Remove connector at index of connectors.
* @param {Int} index
* @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
*/
removeConnectorAt: function(index, preserveConnectedObjs)
{
/*
var connector = this.getConnectors()[index];
if (connector)
{
this.getConnectors().removeAt(index);
this.notifyConnectorsChanged();
}
*/
if (!this.hasCtab())
return null;
return this.getCtab().removeConnectorAt(index, preserveConnectedObjs);
},
/**
* Remove a connector in container.
* @param {Kekule.ChemStructureConnector} connector
* @param {Bool} preserveConnectedObjs Whether delte relations between this connector and related nodes.
*/
removeConnector: function(connector, preserveConnectedObjs)
{
/*
var index = this.getConnectors().indexOf(connector);
if (index >= 0)
this.removeConnectorAt(index);
*/
if (!this.hasCtab())
return null;
return this.getCtab().removeConnector(connector, preserveConnectedObjs);
},
/**
* Remove all connectors.
*/
clearConnectors: function()
{
if (this.hasCtab())
return this.getCtab().clearConnectors();
},
/**
* Insert obj before refChild in node or connector list of ctab.
* If refChild is null or does not exists, obj will be append to tail of list.
* @param {Variant} obj A node or connector.
* @param {Variant} refChild Ref node or connector
* @return {Int} Index of obj after inserting.
*/
insertBefore: function(obj, refChild)
{
if (this.hasCtab())
return this.getCtab().insertBefore(obj, refChild);
/*
else
console.log('no ctab');
*/
},
/**
* Returns nodes or connectors that should be removed cascadely with childObj.
* @param {Object} childObj
* @returns {Array}
* @private
*/
_getObjsNeedToBeCascadeRemoved: function(childObj, ignoredChildObjs)
{
if (this.hasCtab())
return this.getCtab()._getObjsNeedToBeCascadeRemoved(childObj, ignoredChildObjs);
else
return [];
},
/**
* Remove childObj from connection table.
* @param {Variant} childObj A child node or connector.
* @param {Bool} cascadeRemove Whether remove related objects (e.g., bond connected to an atom).
* @param {Bool} freeRemoved Whether free all removed objects.
*/
removeChildObj: function(childObj, cascadeRemove, freeRemoved)
{
if (this.hasCtab())
this.getCtab().removeChildObj(childObj, cascadeRemove, freeRemoved);
},
/**
* Remove child obj directly from connection table.
* @param {Variant} childObj A child node or connector.
*/
removeChild: function($super, obj)
{
return this.removeChildObj(obj) || $super(obj);
},
/**
* Check if childObj is a child node or connector of this fragment's ctab.
* @param {Kekule.ChemObject} childObj
* @returns {Bool}
*/
hasChildObj: function(childObj)
{
if (this.hasCtab())
{
return this.getCtab().hasChildObj(childObj);
}
else
return false;
},
/**
* Returns next sibling node or connector to childObj.
* @param {Variant} childObj Node or connector.
* @returns {Variant}
*/
getNextSiblingOfChild: function(childObj)
{
if (this.hasCtab())
return this.getCtab().getNextSiblingOfChild(childObj);
else
return null;
},
/**
* Get count of child objects (including both nodes and connectors).
* @returns {Int}
*/
getChildCount: function()
{
if (this.hasCtab())
return this.getCtab().getChildCount();
else
return 0;
},
/**
* Get child object (including both nodes and connectors) at index.
* @param {Int} index
* @returns {Variant}
*/
getChildAt: function(index)
{
if (this.hasCtab())
return this.getCtab().getChildAt(index);
else
return null;
},
/**
* Get the index of obj in children list.
* @param {Variant} obj
* @returns {Int} Index of obj or -1 when not found.
*/
indexOfChild: function(obj)
{
if (this.hasCtab())
return this.getCtab().indexOfChild(obj);
else
return -1;
}
});
})();