UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,639 lines (1,602 loc) 160 kB
/** * @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