kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,639 lines (1,602 loc) • 160 kB
JavaScript
/**
* @fileoverview
* File for supporting CML (Chemical Markup Language), especially on read/write molecules and reactions.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /lan/xmlJsons.js
* requires /core/kekule.common.js
* requires /core/kekule.elements.js
* requires /core/kekule.electrons.js
* requires /core/kekule.structures.js
* requires /core/kekule.structureBuilder.js
* requires /core/kekule.reactions.js
* requires /utils/kekule.domHelper.js
* requires /io/kekule.io.js
* requires /localization
*/
(function(){
"use strict";
/*
* Default options to read/write CML format data.
* @object
*/
Kekule.globalOptions.add('IO.cml', {
prettyPrint: true,
enableReadRootObjList: true, // if false, when reading <cml> with multiple child root elements, only the first will be returned by reader
defaultRootObjListHolder: Kekule.ChemSpace // when reading root obj list, put all objects into instance of this class
});
/**
* Defines some constants about CML.
* @class
* @private
*/
Kekule.IO.CML = {
CML2CORE_NAMESPACE_URI: 'http://www.xml-cml.org/schema/cml2/core', //'http://www.xml-cml.org/schema',
CML3_SCHEMA_NAMESPACE_URI: 'http://www.xml-cml.org/schema',
ARRAY_VALUE_DELIMITER: ' ',
TYPED_ELEM_NAMES: ['string', 'integer', 'float'],
TYPED_ARRAY_ELEM_NAMES: ['stringArray', 'integerArray', 'floatArray'],
ATOMS_REF_ATTRIBS: ['atomRef', 'atomRefs2', 'atomRefs3', 'atomRefs4', 'atomRefs', 'atomRefArray'],
BONDS_REF_ATTRIBS: ['bondRef', 'bondRefs', 'bondRefArray'],
MOL_REF_ATTRIBS: ['moleculeRef', 'moleculeRefs'],
DEFAULT_VALUE_DELIMITER_PATTERN: /\s+/gm,
};
Kekule.IO.CML.LEGAL_CORE_NAMESPACE_URIS = [
Kekule.IO.CML.CML2CORE_NAMESPACE_URI,
Kekule.IO.CML.CML3_SCHEMA_NAMESPACE_URI
];
/**
* A help class to handle CML
* @class
*/
Kekule.IO.CmlUtils = {
/** @private */
_cmlUnitConvMap: [ // each item is an array of [cmlUnitCoreName, cmlUnitSymbol, kekuleUnitSymbol, isSiUnit(bool)]
['m', 'm', 'm', true],
['second', 's', 'sec', true],
['hour', 'h', 'hr', false],
['kg', 'kg', 'kg', true],
['k', 'K', 'K', true],
['mol', 'mol', 'mol', true],
['candela', 'cd', 'cd', true],
['radian', 'rad', 'rad', true],
['steradian', 'sr', 'sr', true],
['hertz', 'Hz', 'Hz', true],
['newton', 'N', 'N', true],
['joule', 'J', 'J', true],
['watt', 'W', 'W', true],
['pascal', 'Pa', 'Pa', true],
['coulomb', 'C', 'C', true],
['volt', 'V', 'V', true],
['ampere', 'A', 'A', true],
['ohm', '[Omega]', 'Ω', true],
['farad', 'F', 'F', true],
['siemens', 'S', 'S', true],
['weber', 'Wb', 'Wb', true],
['tesla', 'T', 'T', true],
['henry', 'H', 'H', true],
['becquerel', 'Bq', 'Bq', true],
['gray', 'Gy', 'Gy', true],
['sievert', 'Sv', 'Sv', true],
['katal', 'kat', 'kat', true],
['molarity', '_i_M__i_', 'mol/L', true],
['molality', '_i_m__i_', 'mol/kg', true],
['m2', 'm2', 'm2', true],
['m3', 'm3', 'm3', true],
['m.s-1', 'm.s-1', 'm/s', true],
['m.s-2', 'm.s-2', 'm·s-2', true],
['rad.s-1', 'rad.s-1', 'rad/s', true],
['n.s', 'N.s', 'N·s', true],
['n.m.s', 'N.m.s', 'N·m·s', true],
['n.m', 'N.m', 'N·m', true],
['m-1', 'm-1', 'm-1', true],
['kg.m-3', 'kg.m-3', 'kg·m-3', true],
['kg-1.m3', 'kg-1.m3', 'kg-1·m3', true],
['m-3.mol', 'm-3.mol', 'm-3·mol', true],
['m3.mol-1', 'm3.mol-1', 'm3/mol', true],
['j.k-1', 'J.K-1', 'J/K', true],
['j.k-1.mol-1', 'J.K-1.mol-1', 'J·K-1·mol-1', true],
['j.k-1.kg-1', 'J.K-1.kg-1', 'J·K-1·kg-1', true],
['j.mol-1', 'J.mol-1', 'J/mol', true],
['j.kg-1', 'J.kg-1', 'J/kg', true],
['j.m-3', 'J.m-3', 'J·m-3', true],
['n.m-1', 'N.m-1 = J.m-2', 'N/m', true],
['w.m-2', 'W.m-2', 'W·m-2', true],
['w.m-1.k-1', 'W.m-1.K-1', 'W·m-1·K-1', true],
['m2.s-1', 'm2.s-1', 'm2/s', true],
['pa.s', 'Pa.s', 'Pa·s', true],
['pa.s', 'N.s.m-2', 'Pa·s', true],
['c.m-3', 'C.m-3', 'C·m-3', true],
['a.m-2', 'A.m-2', 'A·m-2', true],
['s.m-1', 'S.m-1', 'S/m', true],
['s.m2.mol-1', 'S.m2.mol-1', 'S·m2/mol', true],
['f.m-1', 'F.m-1', 'F/m', true],
['h.m-1', 'H.m-1', 'H/m', true],
['v.m-1', 'V.m-1', 'V/m', true],
['a.m-1', 'A.m-1', 'A/m', true],
['cd.m-2', 'cd.m-2', 'cd·m-2', true],
['c.kg-1', 'C.kg-1', 'C/kg', true],
['gy.s-1', 'Gy.s-1', 'Gy/s', true],
['j.m-1', 'J.m-1', 'J/m', true],
['ang', '[Aring]', 'Å', false],
['deg', '[deg]', 'deg', false],
['ang3', 'A3', 'Å3', false],
['celsius', '[deg]C', '℃', false],
['debye', 'D', 'D', false],
['electron', 'e', 'e', false],
['hartree', 'hart', 'hart', false],
['gpa', 'GPa', 'GPa', false],
['gpa-1', 'GPa-1', 'GPa-1', false],
['atmosphere', 'atm', 'atm', false],
['kcal.mol-1.ang-1', 'kcal mol-1 ang-1', 'kcal·mol-1·ang-1', false],
['kj.mol-1', 'kj mol-1', 'kj/mol', false],
['l', 'L', 'L', false],
['ml', 'mL', 'mL', false],
['kcal.ang-1', 'kcal.ang-1', 'kcal/Å', false],
['kcal.rad-1', 'kcal.rad-1', 'kcal/rad', false],
['ph', 'pH', 'pH', false],
['centipoise', 'cp', 'cp', false],
['km.s-1', 'km/s', 'km/s', false],
['arbitrary', 'arb', 'arb', false],
],
/*
* Convert a JS type name to XSD type name to be used in <scalar>
* @param {String} dataTypeName
* returns {String}
*/
/*
dataTypeToXsdType: function(dataTypeName)
{
var result =
(dataTypeName == DataType.INT)? 'xsd:integer':
(dataTypeName == DataType.FLOAT)? 'xsd:float':
(dataTypeName == DataType.BOOL)? 'xsd:boolean':
(dataTypeName == DataType.DATE)? 'xsd:dateTime':
null;
}
*/
/**
* Try convert a string to float. If fails, the original string will be returned.
* @param {String} str
* @returns {Variant}
*/
tryParseFloat: function(str)
{
var result = parseFloat(str);
return Kekule.NumUtils.isNormalNumber(result)? result: str;
},
/**
* Based on cml datatype attribute, convert a simple string value to a proper js value.
* @param {String} sValue
* @param {String} xmlDataType
* @param {Bool} forceAllNumTypesToFloat If true, numbers will always be converted with parseFloat rather than parseInt for integer types.
* @returns {Variant}
*/
convertSimpleValueByDataType: function(sValue, xmlDataType, forceAllNumTypesToFloat)
{
var value;
switch (xmlDataType)
{
case 'xsd:boolean':
{
value = Kekule.StrUtils.strToBool(sValue);
break;
}
case 'xsd:float':
case 'xsd:double':
case 'xsd:duration':
case 'xsd:decimal':
{
value = parseFloat(sValue);
break;
}
case 'xsd:integer':
case 'xsd:nonPositiveInteger':
case 'xsd:negativeInteger':
case 'xsd:long':
case 'xsd:int':
case 'xsd:short':
case 'xsd:byte':
case 'xsd:nonNegativeInteger':
case 'xsd:unsignedLong':
case 'xsd:unsignedInt':
case 'xsd:unsignedShort':
case 'xsd:unsignedByte':
case 'xsd:positiveInteger':
{
value = parseInt(sValue);
break;
}
default:
value = sValue;
}
return value;
},
/**
* Returns the CML data type token for a Kekule data type.
* @param {String} dataType
* @returns {String}
*/
getCmlTypeForDataType: function(dataType)
{
var result =
(dataType == DataType.INT)? 'xsd:integer':
(dataType == DataType.FLOAT)? 'xsd:float':
(dataType == DataType.BOOL)? 'xsd:boolean':
null;
return result;
},
/**
* Convert a namespaced string (xxx:yyyy) in CML to Kekule form.
* @param {String} s
* @returns {String}
*/
cmlNsTokenToKekule: function(s)
{
return s.replace(/\:/g, '.');
},
/**
* Convert a namespaced string (xxx.yyyy) in Kekule to CML form.
* @param {String} s
* @returns {String}
*/
kekuleNsTokenToCml: function(s)
{
return s.replace(/\./g, ':');
},
/**
* Returns the namespace and local part of cml namespaced value.
* E.g., returns {namespace: 'units', localName: 'hz'} for 'units:hz'.
* @param {String} s
* @returns {String}
*/
getCmlNsValueDetails: function(s)
{
var p = s.indexOf(':');
var namespace = (p >= 0)? s.substring(0, p): '';
var localName = (p >= 0)? s.substr(p + 1): s;
return {'namespace': namespace, 'localName': localName};
},
/**
* Returns the local part of cml namespaced value.
* E.g., returns 'hz' for 'units:hz'.
* @param {String} s
* @returns {String}
*/
getCmlNsValueLocalPart: function(s)
{
var p = s.indexOf(':');
var localName = (p >= 0)? s.substr(p + 1): s;
return localName;
},
/**
* Get the suitable metric unit symbol for CML unit string.
* @param {String} cmlUnit
* @returns {String}
*/
cmlUnitStrToMetricsUnitSymbol: function(cmlUnit)
{
var coreUnit = Kekule.IO.CmlUtils.getCmlNsValueLocalPart(cmlUnit);
var coreUnitLower = coreUnit.toLowerCase();
// sometimes the unit name is suffixed with plural 's', we may need to remove it?
var coreUnitLowerWithNoSuffix = ((coreUnitLower.length > 3) && coreUnitLower.endsWith('s'))? coreUnitLower.substr(0, coreUnitLower.length - 1): null;
var KU = Kekule.Unit;
/*
var maps = [
['arbitrary', KU.Arbitrary.ARBITRARY],
['counts', KU.Counts.COUNTS],
['cm-1', KU.WaveNumber.RECIPROCAL_CENTIMETER],
['hertz', KU.Frequency.HERTZ],
['hz', KU.Frequency.HERTZ]
];
*/
var maps = Kekule.IO.CmlUtils._cmlUnitConvMap;
var sunit;
for (var i = 0, l = maps.length; i < l; ++i)
{
//if (coreUnitLower === maps[i][0].toLowerCase())
if (coreUnit === maps[i][0] || coreUnit === maps[i][1])
{
sunit = maps[i][2];
break;
}
}
if (!sunit)
{
for (var i = 0, l = maps.length; i < l; ++i)
{
if (coreUnitLower === maps[i][0].toLowerCase() || coreUnitLower === maps[i][1].toLowerCase())
{
sunit = maps[i][2];
break;
}
}
}
if (!sunit && coreUnitLowerWithNoSuffix)
{
for (var i = 0, l = maps.length; i < l; ++i)
{
if (coreUnitLowerWithNoSuffix === maps[i][0].toLowerCase() || coreUnitLowerWithNoSuffix === maps[i][1].toLowerCase())
{
sunit = maps[i][2];
break;
}
}
}
if (!sunit)
{
var unitObj = Kekule.Unit.getUnit(coreUnit, true); // ignore case
sunit = unitObj && unitObj.symbol;
}
if (!sunit)
{
var unitObj = Kekule.Unit.getUnitByName(coreUnit, true); // ignore case
sunit = unitObj && unitObj.symbol;
}
if (!sunit && coreUnitLowerWithNoSuffix)
{
var unitObj = Kekule.Unit.getUnitByName(coreUnitLowerWithNoSuffix, true); // ignore case
sunit = unitObj && unitObj.symbol;
}
if (sunit)
return sunit;
else
return coreUnit; // if unit not found, returns the core unit string directly
},
/**
* Convert a Kekule metrics unit symbol to a suitable CML unit.
* @param {String} unitSymbol
* @returns {String}
*/
metricsUnitSymbolToCmlUnitStr: function(unitSymbol)
{
var maps = Kekule.IO.CmlUtils._cmlUnitConvMap;
var result;
for (var i = 0, l = maps.length; i < l; ++i)
{
var map = maps[i];
//if (coreUnitLower === maps[i][0].toLowerCase())
if (unitSymbol === map[2])
{
var prefix = map[3]? 'siUnits:': 'units:';
result = prefix + map[0];
break;
}
}
if (!result) // not found in map, just use 'unit:symbol' form
result = 'units:' + unitSymbol;
return result;
},
/**
* Turn a CML bond order value to a Kekule one.
* @param {String} cmlOrder
* @returns {Int} Value from {Kekule.BondOrder}
*/
cmlBondOrderToKekule: function(cmlOrder)
{
var result = parseInt(cmlOrder);
if (result !== result) // result is NaN
{
switch (cmlOrder.toUpperCase())
{
case 'A': result = Kekule.BondOrder.EXPLICIT_AROMATIC; break;
case 'D': result = Kekule.BondOrder.DOUBLE; break;
case 'T': result = Kekule.BondOrder.TRIPLE; break;
case 'S':
default:
result = Kekule.BondOrder.SINGLE; break;
}
}
return result;
},
/**
* Turn a Kekule bond order value to a CML string.
* @param {Int} kekuleOrder
* @returns {String}
*/
kekuleBondOrderToCml: function(kekuleOrder)
{
switch (kekuleOrder)
{
case Kekule.BondOrder.EXPLICIT_AROMATIC: return 'A';
case Kekule.BondOrder.QUAD: return '4';
case Kekule.BondOrder.DOUBLE: return 'D';
case Kekule.BondOrder.TRIPLE: return 'T';
//case Kekule.BondOrder.SINGLE: return 'S';
//case Kekule.BondOrder.OTHER: return 'S';
default:
return 'S';
}
},
/**
* Turn a CML bond stereo value to a Kekule one.
* @param {String} cmlStereo
* @returns {Int} Value from {Kekule.BondStereo}
*/
cmlBondStereoToKekule: function(cmlStereo)
{
var result;
switch (cmlStereo.toUpperCase())
{
case 'E': result = Kekule.BondStereo.E; break;
case 'Z': result = Kekule.BondStereo.Z; break;
case 'C': result = Kekule.BondStereo.CIS; break;
case 'T': result = Kekule.BondStereo.TRANS; break;
case 'W': result = Kekule.BondStereo.UP; break;
case 'H': result = Kekule.BondStereo.DOWN; break;
default: result = Kekule.BondStereo.NONE; break;
}
return result;
},
/**
* Turn a Kekule bond stereo value to a CML string.
* Note Kekule has invert stereo enumerations, when translate to CML,
* you have to manually invert the bond atom refs to get a correct stereo result.
* @param {Int} kekuleStereo Value from {Kekule.BondStereo}
* @returns {Hash} {value, invert}
*/
kekuleBondStereoToCml: function(kekuleStereo)
{
switch (kekuleStereo)
{
case Kekule.BondStereo.UP: return {'value': 'W'};
case Kekule.BondStereo.UP_INVERTED: return {'value': 'W', 'invert': true};
case Kekule.BondStereo.DOWN: return {'value': 'H'};
case Kekule.BondStereo.DOWN_INVERTED: return {'value': 'H', 'invert': true};
case Kekule.BondStereo.E: return {'value': 'E'};
case Kekule.BondStereo.Z: return {'value': 'Z'};
case Kekule.BondStereo.CIS: return {'value': 'C'};
case Kekule.BondStereo.TRANS: return {'value': 'T'};
//case Kekule.BondStereo.NONE: return null;
default:
return {'value': null};
}
},
/**
* Turn a CML role value of reactant, product or substance to a Kekule one in {@link Kekule.ReactionRole}.
* If it is not a standard Kekule role, returns the origin value.
* @param {String} cmlRole
* @returns {String}
*/
reagentRoleToKekule: function(cmlRole)
{
var R = Kekule.ReactionRole;
switch (cmlRole)
{
case 'reagent': return R.REAGENT;
case 'catalyst': return R.CATALYST;
case 'solvent': return R.CATALYST;
default: return cmlRole;
}
},
/**
* Turn a Kekule role value of reactant, product or substance to a CML one.
* If it is not a standard CML role, returns the origin value.
* @param {String} kekuleRole
* @returns {String}
*/
kekuleReagentRoleToCml: function(kekuleRole)
{
var R = Kekule.ReactionRole;
switch (kekuleRole)
{
case R.REAGENT: return 'reagent';
case R.CATALYST: return 'catalyst';
case R.SOLVENT: return 'solvent';
default: return kekuleRole;
}
},
/**
* Check if an element type is standing for a dummy atom.
* @param {String} value
* @returns {Bool}
*/
isDummyElementType: function(value)
{
return ((value == 'Du') || (value == 'Dummy'));
},
/**
* Check if an element type is standing for a RGroup.
* @param {String} value
* @returns {Bool}
*/
isRGroupElementType: function(value)
{
return (value == 'R');
},
/**
* Check if an element type is standing for an atom list.
* NOTE: atom list is not native CML concept, just borrowed from MDL.
* @param {String} value
* @returns {Bool}
*/
isAtomListElementType: function(value)
{
return (value == 'L');
},
/**
* Get element symbol (used as elementType in CML) of a {@link Kekule.ChemStructureNode}.
* @param {Kekule.ChemStructureNode} node
* @return {String}
*/
getNodeElementType: function(node)
{
if (node instanceof Kekule.Atom)
return node.getSymbol();
else if (node instanceof Kekule.Pseudoatom)
{
//console.log(node);
switch (node.getAtomType())
{
case Kekule.PseudoatomType.DUMMY: return 'Du';
case Kekule.PseudoatomType.ANY: return 'A';
case Kekule.PseudoatomType.HETERO: return 'Q';
case Kekule.PseudoatomType.CUSTOM:
default:
{
return node.getSymbol();
}
}
}
else if (node instanceof Kekule.VariableAtom)
return 'R'; // 'L'; CML standard has no L
else if (node instanceof Kekule.StructureFragment)
return 'R';
else if (node.getSymbol)
{
return node.getSymbol();
}
else
return '*'; // do not know what's the proper symbol
},
/**
* Create a proper structure node by elementType of CML.
* @param {String} id
* @param {String} elemType
* @param {Number} massNumber Can be null.
* @returns {Kekule.ChemStructureNode}
*/
createNodeByCmdElementType: function(id, elemType, massNumber)
{
var result;
if (Kekule.IO.CmlUtils.isDummyElementType(elemType))
result = new Kekule.Pseudoatom(id, Kekule.PseudoatomType.DUMMY);
else if (Kekule.IO.CmlUtils.isRGroupElementType(elemType))
result = new Kekule.RGroup(id);
else if (Kekule.IO.CmlUtils.isAtomListElementType(elemType))
result = new Kekule.VariableAtom(id);
else if (Kekule.Element.isElementSymbolAvailable(elemType)
|| (elemType == Kekule.Element.UNSET_ELEMENT)) // a normal atom or unset element
{
// in CML, an isotope attribute can be a mass number or a accurate mass, so round it
// here to get a integer mass number
//var massNumber = attribs.isotope? Math.round(attribs.isotope): null;
result = new Kekule.Atom(id, elemType, Math.round(massNumber));
}
else // elemType not a real symbol symbol, create a pseudo atom
{
result = new Kekule.Pseudoatom(id, Kekule.PseudoatomType.CUSTOM, elemType);
}
return result;
}
};
var CmlUtils = Kekule.IO.CmlUtils;
/**
* A help class to do some DOM work on CML
* @class
*/
Kekule.IO.CmlDomUtils = {
/**
* Split a CML array string to a JavaScript array
* @param {Object} value
* @returns {Array}
*/
splitCmlArrayValue: function(value)
{
if ((typeof(value) == 'object') && (value.length)) // already an array
return value;
var s = Kekule.StrUtils.normalizeSpace(Kekule.StrUtils.trim(value));
return s.split(Kekule.IO.CML.ARRAY_VALUE_DELIMITER);
},
/**
* Merge an array to a CML array string.
* @param {Array} arrayObj
* @returns {String}
*/
mergeToCmlArrayValue: function(arrayObj)
{
return arrayObj.join(' ');
},
/**
* Split a CML formula concise value to an array.
* For example, 'S 1 O 4 -2' will be transformed to:
* {'isotopes': [{'elementType': 'S', 'count': 1}, {'elementType': 'O', 'count': 4}], 'formalCharge': -2}
* @param {String} value
* @returns {Hash}
*/
analysisCmlFormulaConciseValue: function(value)
{
var tokens = Kekule.IO.CmlDomUtils.splitCmlArrayValue(value);
var i = 0;
var l = tokens.length;
var isotopes = [];
var currIsotope = null;
var formalCharge;
while (i < l)
{
var token = tokens[i];
var count = parseFloat(token);
if (count != count) // count is NaN, so token is really not a number, first one of pair, should be a symbol
{
if (currIsotope)
isotopes.push(currIsotope);
currIsotope = {'elementType': token};
}
else if (currIsotope) // second of pair, should be the count
{
currIsotope.count = count;
isotopes.push(currIsotope);
currIsotope = null;
}
else if (i == l - 1) // should be the last formal charge
formalCharge = count;
++i;
}
return {'isotopes': isotopes, 'formalCharge': formalCharge};
},
/**
* Get information of CML element with builtin attribute.
* @param {Object} elem
* @param {String} namespaceURI
* @param {Kekule.DomHelper} domHelper
* @returns {Hash} {name, value} while name is the builtin attribute and value is the content of element.
* @private
*/
getCmlBuiltinElemInfo: function(elem, namespaceURI, domHelper)
{
var propName = elem.getAttribute('builtin');
if ((!propName) && namespaceURI)
{
if (elem.getAttributeNS)
propName = elem.getAttributeNS(namespaceURI, 'builtin');
else if (domHelper)
propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
}
if (propName)
{
var propValue = Kekule.DomUtils.getElementText(elem);
return {'name': propName, 'value': propValue};
}
else
return null;
},
/**
* Read element of <string>, <integer> and <float>
* @param {Object} elem
* @param {String} namespaceURI
* @param {Kekule.DomHelper} domHelper
* @returns {Hash} {name, value}
* @private
*/
readCmlTypedPropertyElem: function(elem, namespaceURI, domHelper)
{
var propName;
propName = elem.getAttribute('builtin');
if ((!propName) && namespaceURI)
{
if (elem.getAttributeNS)
propName = elem.getAttributeNS(namespaceURI, 'builtin');
else if (domHelper)
propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
}
if (propName)
{
var propValue = Kekule.DomUtils.getElementText(elem);
switch (Kekule.DomUtils.getLocalName(elem))
{
case 'float':
propValue = parseFloat(propValue);
break;
case 'integer':
propValue = parseInt(propValue);
break;
default:
; // string
}
return {'name': propName, 'value': propValue};
}
else
return null;
},
/**
* Read element of <stringArray>, <integerArray> and <floatArray>
* @param {Object} elem
* @param {String} namespaceURI
* @param {Kekule.DomHelper} domHelper
* @returns {Hash} {name, values: [values]}
* @private
*/
readCmlTypedPropertyArrayElem: function(elem, namespaceURI, domHelper)
{
var propName;
propName = elem.getAttribute('builtin');
if ((!propName) && namespaceURI)
{
if (elem.getAttributeNS)
propName = elem.getAttributeNS(namespaceURI, 'builtin');
else if (domHelper)
propName = domHelper.getAttributeNS(namespaceURI, 'builtin', elem);
}
if (propName)
{
var propValue = Kekule.DomUtils.getElementText(elem);
var values = Kekule.IO.CmlDomUtils.splitCmlArrayValue(propValue);
switch (Kekule.DomUtils.getLocalName(elem))
{
case 'float':
for (var i = 0, l = values.length; i < l; ++i)
values[i] = parseFloat(values[i]);
break;
case 'integer':
for (var i = 0, l = values.length; i < l; ++i)
values[i] = parseInt(values[i]);
break;
default:
; // string
}
return {'name': propName, 'values': values};
}
else
return null;
},
/**
* Check if an element has direct <string><integer> or <float> children
* @param {Object} elem
* @param {String} namespaceURI
* @returns {Bool}
*/
hasDirectCmlTypedElemChildren: function(elem, namespaceURI)
{
for (var i = 0, l = Kekule.IO.CML.TYPED_ELEM_NAMES.length; i < l; ++i)
{
var localName = Kekule.IO.CML.TYPED_ELEM_NAMES[i];
var children = Kekule.DomUtils.getDirectChildElems(elem, null, localName, namespaceURI);
if (children.length > 0)
return true;
}
return false;
},
/**
* Check if an element has direct <stringArray>, <integerArray> or <floatArray> children
* @param {Object} elem
* @param {String} namespaceURI
* @returns {Bool}
*/
hasDirectCmlTypedArrayElemChildren: function(elem, namespaceURI)
{
for (var i = 0, l = Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES.length; i < l; ++i)
{
var localName = Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES[i];
var children = Kekule.DomUtils.getDirectChildElems(elem, null, localName, namespaceURI);
if (children.length > 0)
return true;
}
return false;
},
/**
* Check if an element is <string><integer> or <float>
* @param {Object} elem
* @param {String} namespaceURI Can be null.
* @returns {Bool}
*/
isCmlTypedElem: function(elem, namespaceURI)
{
var result = (Kekule.IO.CML.TYPED_ELEM_NAMES.indexOf(Kekule.DomUtils.getLocalName(elem)) >= 0);
if (namespaceURI)
result = result && (elem.namespaceURI == namespaceURI);
return result;
},
/**
* Check if an element is <stringArray>, <integerArray> or <floatArray>
* @param {Object} elem
* @param {String} namespaceURI Can be null.
* @returns {Bool}
*/
isCmlTypedArrayElem: function(elem, namespaceURI)
{
var result = (Kekule.IO.CML.TYPED_ARRAY_ELEM_NAMES.indexOf(Kekule.DomUtils.getLocalName(elem)) >= 0);
if (namespaceURI)
result = result && (elem.namespaceURI == namespaceURI);
return result;
},
/**
* Check if an element has builtin attribute.
* @param {Object} elem
* @param {String} namespaceURI Can be null.
* @returns {Bool}
*/
isCmlBuiltInMarkedElem: function(elem, namespaceURI)
{
var result = Kekule.DomUtils.hasAttribute(elem, 'builtin');
if (namespaceURI && result)
result = elem.namespaceURI === namespaceURI;
return result;
},
FILTER_TYPED_ELEM: 1,
FILTER_TYPEDARRAY_ELEM: 2,
FILTER_ALL: 3,
/**
* Get child typed element with specified builtinName.
* @param {Object} parent
* @param {String} builtinName
* @param {Int} filter
* @returns {Object} Element found or null.
*/
getCmlTypedElem: function(parent, builtinName, filter)
{
var nsURI = parent.namespaceURI;
var elems = Kekule.DomUtils.getDirectChildElemsOfAttribValues(parent, [{'builtin': builtinName}], null, null, nsURI);
if (elems && (elems.length > 0))
{
for (var i = elems.length - 1; i >= 0; --i)
{
if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedElem(elems[i], nsURI))
return elems[i];
else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(elems[i], nsURI))
return elems[i];
}
}
return null;
},
/**
* Get all child typed elements with specified builtinName.
* @param {Object} parent
* @param {String} builtinName
* @param {Int} filter
* @returns {Array} Elements found or null.
*/
getCmlTypedElems: function(parent, builtinName, filter)
{
var result = [];
var nsURI = parent.namespaceURI;
var elems = Kekule.DomUtils.getDirectChildElemsOfAttribValues(parent, [{'builtin': builtinName}], null, null, nsURI);
if (elems && (elems.length > 0))
{
for (var i = elems.length - 1; i >= 0; --i)
{
if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedElem(elems[i], nsURI))
result.push(elems[i]);
else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(elems[i], nsURI))
result.push(elems[i]);
}
}
return result.length? result: null;
},
/**
* Get value of child typed element with specified builtinName.
* @param {Object} parent
* @param {String} builtinName
* @param {Int} filter
* @returns {Variant} Value found or null.
*/
getCmlTypedElemValue: function(parent, builtinName, filter, domHelper)
{
if (!filter)
filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
var elem = Kekule.IO.CmlDomUtils.getCmlTypedElem(parent, builtinName, filter);
if (elem)
//return Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(elem, parent.namespaceURI, domHelper);
return Kekule.DomUtils.getElementText(elem);
},
/**
* Get value of child typed elements (maybe multiple) with specified builtinName.
* @param {Object} parent
* @param {String} builtinName
* @param {Int} filter
* @returns {Variant} Value found (array) or null.
*/
getMultipleCmlTypedElemValues: function(parent, builtinName, filter, domHelper)
{
if (!filter)
filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
var elems = Kekule.IO.CmlDomUtils.getCmlTypedElems(parent, builtinName, filter);
if (elems)
{
var result = [];
//return Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(elem, parent.namespaceURI, domHelper);
for (var i = 0, l = elems.length; i < l; ++i)
{
result.push(Kekule.DomUtils.getElementText(elems[i]));
}
return result;
}
else
return null;
},
/**
* Get attribute value of elem. If attribName not found in elem's attributes,
* this method will automatically check child <string>, <float> or <integer> elements.
* @param {Object} elem
* @param {String} attribName
* @param {Int} filter
* @returns {Variant} Value of attribute.
*/
getCmlElemAttribute: function(elem, attribName, filter, domHelper)
{
if (!filter)
filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
var result = Kekule.DomUtils.getSameNSAttributeValue(elem, attribName, domHelper);
//if ((result === null) || (result === undefined)) // attrib not found, check child typed elements
if (!result) // attrib not found, check child typed elements
{
result = Kekule.IO.CmlDomUtils.getCmlTypedElemValue(elem, attribName, filter, domHelper);
}
return result;
},
/**
* Get attribute values of (may) multiple elem. If attribName not found in elem's attributes,
* this method will automatically check child <string>, <float> or <integer> elements.
* @param {Object} elem
* @param {String} attribName
* @param {Int} filter
* @returns {Variant} Value of attribute.
*/
getMultipleCmlElemAttribute: function(elem, attribName, filter, domHelper)
{
if (!filter)
filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
var result = Kekule.DomUtils.getSameNSAttributeValue(elem, attribName, domHelper);
//if ((result === null) || (result === undefined)) // attrib not found, check child typed elements
if (!result)
{
result = Kekule.IO.CmlDomUtils.getMultipleCmlTypedElemValues(elem, attribName, filter, domHelper);
}
return result;
},
setCmlElemAttribute: function(elem, attribName, value, domHelper)
{
return Kekule.DomUtils.setSameNSAttributeValue(elem, attribName, value, domHelper);
},
/**
* Fetch all attribute values of CML element into an JSON object, including typed or typed array child elements.
* @param {Object} elem
* @param {Int} filter
* @param {Bool} mergeSameNameTypedElem If multiple child typed elements has same builtin, whether merge their values
* @param {Kekule.DomHelper} domHelper
* @returns {Hash} Each item is a hash: {name: value}.
*/
fetchCmlElemAttributeValuesToJson: function(elem, filter, mergeSameNameTypedElem, domHelper)
{
if (!filter)
filter = Kekule.IO.CmlDomUtils.FILTER_ALL;
var nsURI = elem.namespaceURI;
// get all attributes first
var result = Kekule.DomUtils.fetchAttributeValuesToJson(elem, nsURI, true);
// then check child elements
var childElems = Kekule.DomUtils.getDirectChildElems(elem, null, null, nsURI);
for (var i = 0, l = childElems.length; i < l; ++i)
{
var childElem = childElems[i];
if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM)
&& (Kekule.IO.CmlDomUtils.isCmlTypedElem(childElem, nsURI) || Kekule.IO.CmlDomUtils.isCmlBuiltInMarkedElem(childElem, nsURI)))
{
var obj = Kekule.IO.CmlDomUtils.readCmlTypedPropertyElem(childElem, nsURI, domHelper);
if (obj)
{
if (result[obj.name] && mergeSameNameTypedElem) // multiple elements with same builtin name, merge values
result[obj.name] += Kekule.IO.CML.ARRAY_VALUE_DELIMITER + obj.value;
else
result[obj.name] = obj.value;
}
}
else if ((filter & Kekule.IO.CmlDomUtils.FILTER_TYPEDARRAY_ELEM) && Kekule.IO.CmlDomUtils.isCmlTypedArrayElem(childElem, nsURI))
{
var obj = Kekule.IO.CmlDomUtils.getCmlBuiltinElemInfo(childElem, nsURI, domHelper);
if (obj)
{
if (result[obj.name] && mergeSameNameTypedElem) // multiple elements with same builtin name, merge values
result[obj.name] += Kekule.IO.CML.ARRAY_VALUE_DELIMITER + obj.value;
else
result[obj.name] = obj.value;
}
}
}
return result;
},
/**
* Check is element is <scalar>
* @param {Object} elem
* @param {String} namespaceURI Can be null.
* @returns {Bool}
*/
isScalarElem: function(elem, namespaceURI)
{
var result = (Kekule.DomUtils.getLocalName(elem) == 'scalar');
if (namespaceURI)
result = result && (elem.namespaceURI == namespaceURI);
return result;
//return ((elem.localName == 'scalar') && this.matchCoreNamespace(elem));
},
/**
* Get Id of CML element
* @param {Object} elem
* @returns {String}
*/
getCmlId: function(elem, domHelper)
{
return Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'id', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
},
/**
* Get Id attribute of CML element
* @param {String} id
* @param {Object} elem
*/
setCmlId: function(elem, id, domHelper)
{
return Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'id', id, domHelper);
},
/**
* Get title of CML element
* @param {Object} elem
* @returns {String}
*/
getCmlTitle: function(elem, domHelper)
{
return Kekule.IO.CmlDomUtils.getCmlElemAttribute(elem, 'title', Kekule.IO.CmlDomUtils.FILTER_TYPED_ELEM, domHelper);
},
/**
* Set title attribute of CML element
* @param {Object} elem
* @returns {String}
*/
setCmlTitle: function(elem, title, domHelper)
{
return Kekule.IO.CmlDomUtils.setCmlElemAttribute(elem, 'title', title, domHelper);
}
};
var CmlDomUtils = Kekule.IO.CmlDomUtils;
/**
* A manager to create suitable element reader for CML document.
* @class
*/
Kekule.IO.CmlElementReaderFactory = {
/** @private */
_readers: {},
/**
* Register new reader class.
* @param {Variant} elemLocalName Local name or array of names.
* @param {Object} readerClass Class of reader.
*/
register: function(elemLocalName, readerClass)
{
if (Kekule.ArrayUtils.isArray(elemLocalName))
{
for (var i = 0, l = elemLocalName.length; i < l; ++i)
Kekule.IO.CmlElementReaderFactory.register(elemLocalName[i], readerClass);
}
else
Kekule.IO.CmlElementReaderFactory._readers[elemLocalName.toLowerCase()] = readerClass;
},
/**
* Returns suitable reader for an CML element.
* @param {Variant} elemOrLocalName Element (Object) or element's local name (String).
* @returns {Kekule.IO.CmlElementReader}
*/
getReader: function(elemOrLocalName)
{
var name;
if (typeof(elemOrLocalName) != 'string')
name = Kekule.DomUtils.getLocalName(elemOrLocalName);
else
name = elemOrLocalName;
var readerClass = Kekule.IO.CmlElementReaderFactory._readers[name.toLowerCase()];
if (readerClass)
return new readerClass();
else
return null;
}
};
/**
* A manager to create suitable element writer for CML document.
* @class
*/
Kekule.IO.CmlElementWriterFactory = {
/** @private */
_writers: {},
/**
* Register new writer class.
* @param {Variant} objTypeName Class name or type name or name array of source object.
* @param {Object} writerClass Class of writer.
*/
register: function(objTypeName, writerClass)
{
if (Kekule.ArrayUtils.isArray(objTypeName))
{
for (var i = 0, l = objTypeName.length; i < l; ++i)
Kekule.IO.CmlElementWriterFactory.register(objTypeName[i], writerClass);
}
else
Kekule.IO.CmlElementWriterFactory._writers[objTypeName] = writerClass;
},
/**
* Returns suitable writer.
* @param {Variant} objOrTypeName Object or object's type name.
* @returns {Kekule.IO.CmlElementWriter}
*/
getWriter: function(objOrTypeName)
{
var name;
if (typeof(objOrTypeName) != 'string')
name = DataType.getType(objOrTypeName);
else
name = objOrTypeName;
var writerClass = Kekule.IO.CmlElementWriterFactory._writers[name];
if ((!writerClass) && (objOrTypeName.getClassName)) // not found, check if obj is instance of type
{
var obj = objOrTypeName;
var typeNames = Kekule.ObjUtils.getOwnedFieldNames(Kekule.IO.CmlElementWriterFactory._writers);
for (var i = typeNames.length - 1; i >= 0; --i) // the later the superior
{
var objClass = ClassEx.findClass(typeNames[i]);
//if (obj instanceof eval(typeNames[i]))
if (objClass && (obj instanceof objClass))
{
writerClass = Kekule.IO.CmlElementWriterFactory._writers[typeNames[i]];
break;
}
}
}
if (writerClass)
return new writerClass();
else
return null;
}
};
/**
* Base class for classes to read or write CML element.
* @class
* @augments ObjectEx
*
* @property {String} coreNamespaceURI Namespace URI of CML core.
* @property {Array} namespaces Namespaces used in a CML document. Each item is has fields {prefix, namespaceURI}.
*/
Kekule.IO.CmlElementHandler = Class.create(ObjectEx,
/** @lends Kekule.IO.CmlElementHandler# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.CmlElementHandler',
/** @constructs */
initialize: function()
{
this.tryApplySuper('initialize');
this._childHandlers = []; // a private field to storing all involved child readers
},
/** @ignore */
doFinalize: function()
{
for (var i = this._childHandlers.length - 1; i >= 0; --i)
{
var childHandler = this._childHandlers[i];
if (childHandler && childHandler.finalize)
childHandler.finalize();
}
this.tryApplySuper('doFinalize');
},
/** @private */
initProperties: function()
{
// a private property
this.defineProp('domHelper', {
'dataType': 'Kekule.DomHelper',
'serializable': false,
'getter': function()
{
if (!this.getPropStoreFieldValue('domHelper'))
this.setPropStoreFieldValue('domHelper', new Kekule.DomHelper());
return this.getPropStoreFieldValue('domHelper');
}
});
this.defineProp('coreNamespaceURI', {
'dataType': DataType.STRING, 'serializable': false
// temp
//'getter': function() { return Kekule.IO.CML.CML2CORE_NAMESPACE_URI; },
//'setter': function() {}
});
this.defineProp('namespaces', {
'dataType': DataType.ARRAY, 'serializable': false
});
/*
this.defineProp('reactionNamespaceURI', {
'dataType': DataType.STRING, 'serializable': false,
// temp
'getter': function() { return Kekule.IO.CML.CML2CORE_NAMESPACE_URI; },
'setter': function() {}
});
*/
// private, storing the root object (CmlReader/CmlWriter) that starting the read/write job
this.defineProp('rootEvoker', {
'dataType': DataType.Object, 'serializable': false
});
},
/**
* Returns the defined prefix for a namespace URI.
* @param {String} uri
* @return {String} If prefix not found, undefined will be returned.
* @private
*/
getPrefixForNamespaceUri: function(uri)
{
var namespaces = this.getNamespaces();
if (namespaces)
{
for (var i = 0, l = namespaces.length; i < l; ++i)
{
if (namespaces[i].namespaceURI === uri)
return namespaces[i].prefix;
}
}
return void(0);
},
/** @private */
matchCoreNamespace: function(nodeOrNsURI)
{
var nsURI;
if (!nodeOrNsURI)
nsURI = '';
else if (typeof(nodeOrNsURI) != 'string')
nsURI = nodeOrNsURI.namespaceURI;
else
nsURI = nodeOrNsURI;
return ((!this.getCoreNamespaceURI()) && (!nsURI)
|| (this.getCoreNamespaceURI() && (this.getCoreNamespaceURI() == nsURI)));
},
/**
* Copy domHelper and coreNamespaceURI to child reader or writer.
* @param {Object} childHandler
* @private
*/
copySettingsToChildHandler: function(childHandler)
{
childHandler.setDomHelper(this.getDomHelper());
childHandler.setCoreNamespaceURI(this.getCoreNamespaceURI());
childHandler.setNamespaces(this.getNamespaces());
childHandler.setRootEvoker(this.getRootEvoker());
},
/** @private */
_appendChildHandler: function(handler)
{
Kekule.ArrayUtils.pushUnique(this._childHandlers, handler);
},
/**
* Call function to all child handlers.
* The function has param (handler).
* @param {Func} func
* @param {Object} thisArg
* @param {Bool} cascade If true, the children of child handlers will also be iterated.
* @private
*/
_iterateChildHandlers: function(func, thisArg, cascade)
{
if (func)
{
for (var i = 0, l = this._childHandlers.length; i < l; ++i)
{
var handler = this._childHandlers[i];
if (handler)
{
func.apply(thisArg, [handler]);
if (cascade && handler._iterateChildHandlers)
handler._iterateChildHandlers(func, thisArg, cascade);
}
}
}
}
});
/**
* Base class of readers to read different CML elements.
* @class
* @augments Kekule.IO.CmlElementHandler
*/
Kekule.IO.CmlElementReader = Class.create(Kekule.IO.CmlElementHandler,
/** @lends Kekule.IO.CmlElementReader# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.CmlElementReader',
/**
* Read an element in CML document and returns a proper Kekule object for parent reader or insert something into parentObj (if parentObj is set).
* @param {Object} elem
* @param {Kekule.ChemObject} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @param {Hash} options
* @returns {Variant}
*/
readElement: function(elem, parentObj, parentReader, options)
{
//console.log('read element', this.getClassName(), parentReader && parentReader.getClassName(), options);
var result = this.doReadElement(elem, parentObj, parentReader, options);
if (result && result.getId && (!result.getId()))
{
if (result.setId)
{
var id = Kekule.IO.CmlDomUtils.getCmlId(elem, this.getDomHelper);
if (id)
{
//result.setId(id);
this.setObjId(result, id);
}
}
}
return result;
},
/**
* Method to do the actual readElement job. Descendants should override this method.
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @param {Hash} options
* @returns {Variant}
* @private
*/
doReadElement: function(elem, parentObj, parentReader, options)
{
// do nothing here
},
/**
* Read and handle child elements,
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @returns {Variant}
*/
readChildElement: function(elem, parentObj, parentReader)
{
return this.doReadChildElement(elem, parentObj, parentReader);
},
/**
* The real job of readChildElem is done here. Descendants may override this.
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @returns {Variant}
* @private
*/
doReadChildElement: function(elem, parentObj, parentReader)
{
return this.doReadChildElementDef(elem, parentObj, parentReader);
},
/**
* A default method to read child elements,
* just ask CmlElementReaderFactory to create a suitable reader and use the reader to read.
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @returns {Variant}
*/
readChildElementDef: function(elem, parentObj, parentReader)
{
return this.doReadChildElementDef(elem, parentObj, parentReader);
},
/**
* The real job of readChildElementDef is done here. Descendants may override this.
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @returns {Variant}
* @private
*/
doReadChildElementDef: function(elem, parentObj, parentReader)
{
var reader = this.doGetChildElementReader(elem, parentObj, parentReader); //Kekule.IO.CmlElementReaderFactory.getReader(elem);
if (reader)
{
try
{
//reader.setCoreNamespaceURI(this.getCoreNamespaceURI());
//this.copySettingsToChildHandler(reader);
var result = reader.readElement(elem, parentObj, parentReader);
return result;
}
finally
{
//reader.finalize(); child readers will be released at the finalize method of parent reader
}
}
else
return null;
},
/**
* Get a reader instance to read CML data from child element.
* Descendants may override this method to returns customized reader.
* @param {Element} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @returns {Kekule.CmlElementReader}
* @private
*/
doGetChildElementReader: function(element, parentObj, parentReader)
{
var result = Kekule.IO.CmlElementReaderFactory.getReader(element);
if (result)
{
this.copySettingsToChildHandler(result);
this._appendChildHandler(result);
}
return result;
},
/**
* Iterate through and read all direct children of elem.
* @param {Object} elem
* @param {Object} parentObj
* @param {Kekule.IO.CmdElementReader} parentReader
* @param {Func} callback An optional callback function evoked when a child element is iterated and returned with a result.
* The function has arguments (childElem, childResult, parentObj, parentReader).
* @private
*/
iterateChildElements: function(elem, parentObj, parentReader, callback)
{
var result = [];
var children = Kekule.DomUtils.getDirectChildElems(elem, null, null, this.getCoreNamespaceURI());
for (var i = 0, l = children.length; i < l; ++i)
{
var subResult = this.readChildElement(children[i], parentObj, parentReader);
if (subResult)
{
result.push({'element': children[i], 'result': subResult});
if (callback)
callback(children[i], subResult, parentObj, parentReader);
}
}
return result;
},
/**
* Called after the whole CML document is read and the corresponding chem objects are built.
* @param {Bool} cascade Whether calling the doneReadingDocument method of child readers.
* @private
*/
doneReadingDocument: function(cascade)
{
this.doDoneReadingDocument();
if (cascade)
{
this._iterateChildHandlers(function(handler){
if (handler && handler.doneReadingDocument)
handler.doneReadingDocument(cascade);
});
}
},
/**
* Do the actual work of doneReadingDocument method.
* Descendants may override this method.
* @private
*/
doDoneReadingDocument: function()
{
// do nothing here
},
/**
* From all objects loaded by the root reader, returns the one with id.
* This method should be called in doneReadingDocument method,
* when all the objects in CML document are loaded.
* @param {String} id
* @returns {Object}
* @private
*/
getLoadedObjById: function(id)
{
var root = this.getRootEvoker();
return root && root.getLoadedObjById && root.getLoadedObjById(id);
},
/**
* Add an id-obj pair to the map of root reader.
* @param {String} id
* @param {Object} obj
* @private
*/
setObjIdMapValue: function(id, obj)
{
var root = this.getRootEvoker();
if (root.setObjIdMapValue)
root.setObjIdMapValue(id, obj);
},
/** @private */
setObjId: function(obj, id)
{
if (obj.setId)
obj.setId(id);
var newId = obj.getId && obj.getId();
if (newId)
this.setObjIdMapValue(newId, obj);
},
/**
* Create a parent object to hold all childObjs inside it.
* @param {Array} childObjs
* @param {Class} wrapperClass
* @returns {Object}
* @private
*/
_createChildObjsHolder: function(childObjs, wrapperClass)
{
var result = new wrapperClass();
for (var i = 0, l = childObjs.length; i < l; ++i)
{
result.appendChild(childObjs[i]);
}
return result;
}
});
/**
* Base class of readers to read different CML elements.
* @class
* @augments Kekule.IO.CmlElementHandler
*/
Kekule.IO.CmlElementWriter = Class.create(Kekule.IO.CmlElementHandler,
/** @lends Kekule.IO.CmlElementWriter# */
{
/** @private */
CLASS_NAME: 'Kekule.IO.CmlElementWriter',
/**
* Write Kekule obj to a new CML element in doc or insert new attributes to parentElem.
* @param {Kekule.ChemObject} obj
* @param {Object} parentElem
* @param {Hash} options
* @returns {Object} Element created.
*/
writeObject: function(obj, parentElem, options)
{
if (!(parentElem) && (!doc))
{
Kekule.error(/*Kekule.ErrorMsg.CML_CAN_NOT_OUTPUT_TO_EMPTY_ELEMENT*/Kekule.$L('ErrorMsg.CML_CAN_NOT_OUTPUT_TO_EMPTY_ELEMENT'));
return null;
}
/*
if ((!parentElem) && doc)
parentElem = doc.documentElement;
else if (!doc)
doc = parentElem.ownerDocument;
*/
var doc = parentElem.ownerDocument;
if (this.getDomHelper().getDocument != doc)
this.getDomHelper().setDocument(doc);
var targetElem = this.doCreateElem(obj, parentElem, doc);
if (targetElem)
parentElem.appendChild(targetElem);
else // if no need to create child element, write directly on parentElem
targetElem = parentElem;
var result = this.doWriteObject(obj, targetElem, options) || targetElem;
if (result && obj)
{
// id
this.writeObjId(obj, result);
// scalar & info
this.writeObjAdditionalInfo(obj, result, options);
}
return result;
},
writeObjId: function(obj, elem)
{
if (obj.getId)
{
var id = obj.getId();
if (!id && obj.setId) // force create an id, for the possible ref for other objects
{
id = this.autoIdentifyForObj(obj);
}
Kekule.IO.CmlDomUtils.setCmlId(elem, id, this.getDomHelper());
}
},
/**
* Gives obj an auto id if its id is not explicit set.
* @private
*/
autoIdentifyForObj: function(obj)
{
if (obj.getId && !obj.getId())
{
if (obj.setId)
{
if (obj.getOwner && obj.getOwner() && obj.getOwner().getAutoId) // use owner's auto id function
obj.setId(obj.getOwner().getAutoId(obj));
else
obj.se