001package com.hfg.chem; 002 003import java.util.ArrayList; 004import java.util.Collection; 005import java.util.Collections; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Map; 010import java.util.Set; 011import java.util.Stack; 012 013import com.hfg.bio.HfgBioXML; 014import com.hfg.bio.PhysicalProperty; 015import com.hfg.exception.UnmodifyableObjectException; 016import com.hfg.util.AttributeMgr; 017import com.hfg.util.CharUtil; 018import com.hfg.util.CompareUtil; 019import com.hfg.util.collection.CollectionUtil; 020import com.hfg.util.StringUtil; 021import com.hfg.xml.HfgXML; 022import com.hfg.xml.HfgXMLSerializable; 023import com.hfg.xml.XMLAttribute; 024import com.hfg.xml.XMLNode; 025import com.hfg.xml.XMLTag; 026 027//------------------------------------------------------------------------------ 028/** 029 Generic chemical entity. 030 <div> 031 @author J. Alex Taylor, hairyfatguy.com 032 </div> 033 */ 034//------------------------------------------------------------------------------ 035// com.hfg XML/HTML Coding Library 036// 037// This library is free software; you can redistribute it and/or 038// modify it under the terms of the GNU Lesser General Public 039// License as published by the Free Software Foundation; either 040// version 2.1 of the License, or (at your option) any later version. 041// 042// This library is distributed in the hope that it will be useful, 043// but WITHOUT ANY WARRANTY; without even the implied warranty of 044// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 045// Lesser General Public License for more details. 046// 047// You should have received a copy of the GNU Lesser General Public 048// License along with this library; if not, write to the Free Software 049// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 050// 051// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 052// jataylor@hairyfatguy.com 053//------------------------------------------------------------------------------ 054 055public class Molecule implements Matter, HfgXMLSerializable, Cloneable, Comparable 056{ 057 058 //########################################################################## 059 // PRIVATE FIELDS 060 //########################################################################## 061 062 //private Map<Element, Float> mElementalComposition; 063 private ElementalComposition mElementalComposition; 064 private Double mMonoisotopicMass; 065 private Double mAverageMass; 066 private Double mOrganicAverageMass; 067 private String mChemicalFormula; 068 069 private boolean mMonoisotopicMassIsUserSet; 070 private boolean mAverageMassIsUserSet; 071 private boolean mOrganicAverageMassIsUserSet; 072 073 private Integer mHashCode; 074 075 private List<IonizableGroup> mKas; 076 private Map<PhysicalProperty, Double> mPropertyMap; 077 private AttributeMgr mAttributeMgr; 078 079 // Optionally, individual atoms can be specified instead of 080 // general numbers of elements. 081 private List<Atom> mAtoms; 082 083 protected String mName; 084 protected boolean mLocked; 085 086 087// protected OrganicMatterImpl mMatter = new OrganicMatterImpl(); 088 089 private static final Set<Character> sSaltIndicatorsInChemicalFormulas = new HashSet<>(4); 090 091 static 092 { 093 // These are the various symbols that can precede hydration notation 094 sSaltIndicatorsInChemicalFormulas.add('.'); 095 sSaltIndicatorsInChemicalFormulas.add('·'); 096 sSaltIndicatorsInChemicalFormulas.add('•'); 097 sSaltIndicatorsInChemicalFormulas.add('*'); 098 } 099 100 //########################################################################## 101 // PUBLIC FIELDS 102 //########################################################################## 103 104 public static final Molecule H2O = new Molecule().setName("water").setChemicalFormula("Hâ‚‚O").lock(); 105 public static final Molecule NH3 = new Molecule().setName("ammonia").setChemicalFormula("NH₃").lock(); 106 public static final Molecule NaCl = new Molecule().setName("sodium chloride").setChemicalFormula("NaCl").lock(); 107 108 //########################################################################## 109 // CONSTRUCTORS 110 //########################################################################## 111 112 //-------------------------------------------------------------------------- 113 public Molecule() 114 { 115 } 116 117 //-------------------------------------------------------------------------- 118 public Molecule(String inName) 119 { 120 this(inName, null); 121 } 122 123 //-------------------------------------------------------------------------- 124 public Molecule(Map<Element, Float> inElementalComposition) 125 { 126 this(null, inElementalComposition); 127 } 128 129 //-------------------------------------------------------------------------- 130 public Molecule(String inName, Map<Element, Float> inElementalComposition) 131 { 132 mName = inName; 133 setElementalComposition(inElementalComposition); 134 } 135 136 //-------------------------------------------------------------------------- 137 public Molecule(Matter inInitialValue) 138 { 139 add(inInitialValue); 140 } 141 142 143 //-------------------------------------------------------------------------- 144 public Molecule(XMLNode inXML) 145 { 146 mName = inXML.getAttributeValue(HfgBioXML.NAME_ATT); 147 148 XMLNode compTag = inXML.getOptionalSubtagByName(HfgBioXML.ELEMENTAL_COMP_TAG); 149 if (compTag != null) 150 { 151 for (XMLAttribute attr : compTag.getAttributes()) 152 { 153 addAtoms(Element.valueOf(attr.getName()), Float.parseFloat(attr.getValue())); 154 } 155 } 156 157 XMLNode attributesTag = inXML.getOptionalSubtagByName(HfgXML.ATTRIBUTES); 158 if (attributesTag != null) 159 { 160 mAttributeMgr = new AttributeMgr(attributesTag); 161 } 162 } 163 164 165 //########################################################################## 166 // PUBLIC METHODS 167 //########################################################################## 168 169 //-------------------------------------------------------------------------- 170 public Molecule setName(String inValue) 171 { 172 mName = inValue; 173 return this; 174 } 175 176 //-------------------------------------------------------------------------- 177 public String name() 178 { 179 return mName; 180 } 181 182 //-------------------------------------------------------------------------- 183 @Override 184 public String toString() 185 { 186 return name(); 187 } 188 189 //-------------------------------------------------------------------------- 190 @Override 191 public int hashCode() 192 { 193 if (null == mHashCode) 194 { 195 int hashcode = 31; 196 if (mElementalComposition != null) 197 { 198 hashcode += mElementalComposition.hashCode(); 199 } 200 201 if (mName != null) 202 { 203 hashcode = hashcode + 31 * mName.hashCode(); 204 } 205 206 mHashCode = hashcode; 207 } 208 209 return mHashCode; 210 } 211 212 213 //-------------------------------------------------------------------------- 214 @Override 215 public boolean equals(Object inObj) 216 { 217 boolean result = false; 218 219 if (inObj != null) 220 { 221 result = (0 == compareTo(inObj)); 222 } 223 224 return result; 225 } 226 227 //-------------------------------------------------------------------------- 228 @Override 229 public int compareTo(Object inObj) 230 { 231 int result = -1; 232 233 if (inObj instanceof Molecule) 234 { 235 result = CompareUtil.compare(mElementalComposition, ((Molecule) inObj).mElementalComposition); 236 } 237 238 return result; 239 } 240 241 //-------------------------------------------------------------------------- 242 /** 243 Returns an unlocked copy of the Molecule. 244 @return an unlocked copy of the Molecule 245 */ 246 public Molecule clone() 247 { 248 Molecule copy; 249 try 250 { 251 copy = (Molecule) super.clone(); 252 } 253 catch (CloneNotSupportedException e) 254 { 255 throw new RuntimeException("Coding problem! CloneNotSupportedException should not be possible when cloning a " 256 + this.getClass().getSimpleName() + " object!", e); 257 } 258 259 if (mElementalComposition != null) 260 { 261 copy.mElementalComposition = new ElementalComposition(mElementalComposition); 262 } 263 264 if (mPropertyMap != null) 265 { 266 copy.mPropertyMap = new HashMap<>(mPropertyMap); 267 } 268 269 if (mAttributeMgr != null) 270 { 271 // For right now this is a shallow clone of the attributes 272 copy.mAttributeMgr = mAttributeMgr.clone(); 273 } 274 275 // Clones should be unlocked. 276 copy.mLocked = false; 277 278 return copy; 279 } 280 281 //-------------------------------------------------------------------------- 282 public boolean isLocked() 283 { 284 return mLocked; 285 } 286 287 //-------------------------------------------------------------------------- 288 public Molecule lock() 289 { 290 mLocked = true; 291 return this; 292 } 293 294 //-------------------------------------------------------------------------- 295 public void setElementalComposition(Map<Element, Float> inMap) 296 { 297 mElementalComposition = null; 298 if (CollectionUtil.hasValues(inMap)) 299 { 300 mElementalComposition = new ElementalComposition(inMap); 301 } 302 303 clearCalculatedProperties(); 304 } 305 306 //-------------------------------------------------------------------------- 307 public void clearElementalComposition() 308 { 309 if (mElementalComposition != null) 310 { 311 mElementalComposition.clear(); 312 } 313 314 clearCalculatedProperties(); 315 } 316 317 //-------------------------------------------------------------------------- 318 public Molecule addElementalComposition(Map<Element, Float> inMap) 319 { 320 return addElementalComposition(inMap, 1); 321 } 322 323 //-------------------------------------------------------------------------- 324 public Molecule addElementalComposition(Map<Element, Float> inMap, int inNum) 325 { 326 if (CollectionUtil.hasValues(inMap)) 327 { 328 if (mElementalComposition != null) 329 { 330 mElementalComposition.add(inMap, inNum); 331 } 332 else 333 { 334 mElementalComposition = new ElementalComposition(inMap, inNum); 335 } 336 337 clearCalculatedProperties(); 338 } 339 340 return this; 341 } 342 343 //-------------------------------------------------------------------------- 344 public Molecule addElementalComposition(ElementalComposition inElementalComposition, int inNum) 345 { 346 if (inElementalComposition != null) 347 { 348 if (mElementalComposition != null) 349 { 350 mElementalComposition.add(inElementalComposition, inNum); 351 } 352 353 clearCalculatedProperties(); 354 } 355 356 return this; 357 } 358 359 //-------------------------------------------------------------------------- 360 public Molecule add(Matter inValue) 361 { 362 return add(inValue, 1); 363 } 364 365 //-------------------------------------------------------------------------- 366 public Molecule add(Matter inValue, int inCount) 367 { 368 if (inValue != null) 369 { 370 // This will also clear calculated properties 371 addElementalComposition(inValue.getElementalComposition(), inCount); 372 373 374 // Now deal with user-set masses... 375 376 if (mMonoisotopicMassIsUserSet 377 || (null == inValue.getElementalComposition() 378 && inValue.getMonoisotopicMass() != null)) 379 { 380 setMonoisotopicMass((mMonoisotopicMassIsUserSet ? mMonoisotopicMass : CollectionUtil.hasValues(getElementalComposition()) ? getMonoisotopicMass() : 0.0) 381 + (inValue.getMonoisotopicMass() != null ? inValue.getMonoisotopicMass() : 0) * inCount); 382 } 383 384 if (mAverageMassIsUserSet 385 || (null == inValue.getElementalComposition() 386 && inValue.getAverageMass() != null)) 387 { 388 setAverageMass((mAverageMassIsUserSet ? mAverageMass : CollectionUtil.hasValues(getElementalComposition()) ? getAverageMass() : 0.0) 389 + (inValue.getAverageMass() != null ? inValue.getAverageMass() : 0) * inCount); 390 } 391 392 if (mOrganicAverageMassIsUserSet 393 || (null == inValue.getElementalComposition() 394 && inValue.getOrganicAverageMass() != null)) 395 { 396 setOrganicAverageMass((mOrganicAverageMassIsUserSet ? mOrganicAverageMass : CollectionUtil.hasValues(getElementalComposition()) ? getOrganicAverageMass() : 0.0) 397 + (inValue.getOrganicAverageMass() != null ? inValue.getOrganicAverageMass() : 0) * inCount); 398 } 399 } 400 401 return this; 402 } 403 404 //-------------------------------------------------------------------------- 405 public Molecule remove(Matter inValue) 406 { 407 return remove(inValue, 1); 408 } 409 410 //-------------------------------------------------------------------------- 411 public Molecule remove(Matter inValue, int inCount) 412 { 413 return add(inValue, - inCount); 414 } 415 416 //-------------------------------------------------------------------------- 417 public Molecule addAtoms(Element inElement, int inNum) 418 { 419 return addAtoms(inElement, new Float(inNum)); 420 } 421 422 //-------------------------------------------------------------------------- 423 public Molecule addAtoms(Element inElement, float inNum) 424 { 425 if (null == mElementalComposition) 426 { 427 mElementalComposition = new ElementalComposition(); 428 } 429 430 Float count = mElementalComposition.get(inElement); 431 float newCount = inNum + (count != null ? count : 0); 432 433 mElementalComposition.put(inElement, newCount); 434 435 clearCalculatedProperties(); 436 437 return this; 438 } 439 440 441 //-------------------------------------------------------------------------- 442 public Molecule addAtom(Atom inAtom) 443 { 444 if (null == mAtoms) 445 { 446 mAtoms = new ArrayList<>(20); 447 } 448 449 mAtoms.add(inAtom); 450 451 // Update the elemental composition 452 return addAtoms(inAtom.getElement(), 1); 453 } 454 455 //-------------------------------------------------------------------------- 456 public Molecule addAtoms(List<Atom> inAtoms) 457 { 458 if (CollectionUtil.hasValues(inAtoms)) 459 { 460 if (null == mAtoms) 461 { 462 mAtoms = new ArrayList<>(20); 463 } 464 465 mAtoms.addAll(inAtoms); 466 467 // Update the elemental composition 468 for (Atom atom : inAtoms) 469 { 470 addAtoms(atom.getElement(), 1); 471 } 472 } 473 474 return this; 475 } 476 477 //-------------------------------------------------------------------------- 478 public List<Atom> getAtoms() 479 { 480 return mAtoms != null ? Collections.unmodifiableList(mAtoms) : null; 481 } 482 483 //-------------------------------------------------------------------------- 484 public Atom getLastAtom() 485 { 486 Atom atom = null; 487 if (CollectionUtil.hasValues(mAtoms)) 488 { 489 atom = mAtoms.get(mAtoms.size() - 1); 490 } 491 492 return atom; 493 } 494 495 //-------------------------------------------------------------------------- 496 /** 497 If the elemental composition is known, use setElementalComposition() and 498 the masses will be derived automatically; this method is for use in those 499 (hopefully) rare times when the mass is known but not the elemental composition. 500 @param inValue the mass to use as the monoisotopic mass for this object 501 @return this Molecule object to enable method chaining 502 */ 503 public Molecule setMonoisotopicMass(Double inValue) 504 { 505 mMonoisotopicMass = inValue; 506 mMonoisotopicMassIsUserSet = (inValue != null); 507 return this; 508 } 509 510 //-------------------------------------------------------------------------- 511 public Double getMonoisotopicMass() 512 { 513 if (null == mMonoisotopicMass) 514 { 515 calculateMassFromElementalComposition(); 516 } 517 518 return mMonoisotopicMass; 519 } 520 521 522 //-------------------------------------------------------------------------- 523 /** 524 If the elemental composition is known, use setElementalComposition() and 525 the masses will be derived automatically; this method is for use in those 526 (hopefully) rare times when the mass is known but not the elemental composition. 527 @param inValue the mass to use as the average mass for this object 528 @return this Molecule object to enable method chaining 529 */ 530 public Molecule setAverageMass(Double inValue) 531 { 532 mAverageMass = inValue; 533 mAverageMassIsUserSet = (inValue != null); 534 return this; 535 } 536 537 //-------------------------------------------------------------------------- 538 @Override 539 public Double getAverageMass() 540 { 541 if (null == mAverageMass) 542 { 543 calculateMassFromElementalComposition(); 544 } 545 546 return mAverageMass; 547 } 548 549 //-------------------------------------------------------------------------- 550 /** 551 If the elemental composition is known, use setElementalComposition() and 552 the masses will be derived automatically; this method is for use in those 553 (hopefully) rare times when the mass is known but not the elemental composition. 554 @param inValue the mass to use as the organic average mass for this object 555 @return this Molecule object to enable method chaining 556 */ 557 public Molecule setOrganicAverageMass(Double inValue) 558 { 559 mOrganicAverageMass = inValue; 560 561 mOrganicAverageMassIsUserSet = (inValue != null); 562 563 return this; 564 } 565 566 //-------------------------------------------------------------------------- 567 @Override 568 public Double getOrganicAverageMass() 569 { 570 if (null == mOrganicAverageMass) 571 { 572 calculateMassFromElementalComposition(); 573 } 574 575 return mOrganicAverageMass; 576 } 577 578 579 //-------------------------------------------------------------------------- 580 /** 581 Returns the elemental composition as an unmodifiable Map. 582 */ 583 @Override 584 public Map<Element, Float> getElementalComposition() 585 { 586 return (mElementalComposition != null ? mElementalComposition.toMap() : null); 587 } 588 589 //-------------------------------------------------------------------------- 590 public Molecule setChemicalFormula(String inValue) 591 { 592 mChemicalFormula = inValue; 593 594 if (StringUtil.isSet(inValue)) 595 { 596 Stack<Character> enclosingCharStack = new Stack<>(); 597 598 FormulaSubBlock currentSubBlock = new FormulaSubBlock(); 599 FormulaSubBlock topBlock = currentSubBlock; 600 Stack<FormulaSubBlock> blockStack = new Stack<>(); 601 blockStack.push(topBlock); 602 603 String chemicaFormulaString = inValue.trim(); 604 605 for (int i = 0; i < chemicaFormulaString.length(); i++) 606 { 607 char currentChar = chemicaFormulaString.charAt(i); 608 609 if ('(' == currentChar 610 || '[' == currentChar) 611 { 612 enclosingCharStack.push(currentChar); 613 FormulaSubBlock subBlock = new FormulaSubBlock().setBracketType(currentChar); 614 currentSubBlock.addSubBlock(subBlock); 615 blockStack.push(subBlock); 616 617 currentSubBlock = subBlock; 618 } 619 else if (')' == currentChar 620 || ']' == currentChar) 621 { 622 // Ending enclosure w/o a starting enclosure? 623 if (0 == enclosingCharStack.size()) 624 { 625 throw new ChemicalFormulaParseException("The chemical formula " + StringUtil.singleQuote(chemicaFormulaString) + " has an unbalanced '" + currentChar + "' near position " + (i + 1) + "!"); 626 } 627 628 char openingChar = enclosingCharStack.pop(); 629 // Mismatched enclosure characters? 630 if (('(' == openingChar && ')' != currentChar) 631 || ('[' == openingChar && ']' != currentChar)) 632 { 633 throw new ChemicalFormulaParseException("The chemical formula " + StringUtil.singleQuote(chemicaFormulaString) + " has an unbalanced '" + currentChar + "' near position " + (i + 1) + "!"); 634 } 635 636 currentSubBlock.close(); 637 638 i++; 639 640 String countString = ""; 641 Character convertedSubscriptChar = null; 642 while (i < chemicaFormulaString.length()) 643 { 644 char countChar = chemicaFormulaString.charAt(i); 645 if (Character.isDigit(countChar) 646 || (convertedSubscriptChar = convertSubscriptChar(countChar)) != null) 647 { 648 countString += (convertedSubscriptChar != null ? convertedSubscriptChar : countChar); 649 i++; 650 } 651 else 652 { 653 break; 654 } 655 } 656 657 if (StringUtil.isSet(countString)) 658 { 659 currentSubBlock.setCount(Integer.parseInt(countString)); 660 } 661 662 if (i < chemicaFormulaString.length()) 663 { 664 i--; 665 } 666 667 blockStack.pop(); 668 currentSubBlock = blockStack.peek(); 669 } 670 else 671 { 672 currentSubBlock.append(currentChar); 673 } 674 } 675 676 setElementalComposition(topBlock.getChemicalComposition()); 677 } 678 679 return this; 680 } 681 682 //-------------------------------------------------------------------------- 683 /** 684 Returns a chemical formula String like 'C5H11NO'. If carbon is present, it 685 is listed first followed by the other elements in ascending mass order. 686 Symbols for isotopes are enclosed in square brackets such as '[2H]2O' 687 for deuterated water. 688 @return the chemical formula string 689 */ 690 public String getChemicalFormula() 691 { 692 if (null == mChemicalFormula) 693 { 694 mChemicalFormula = (mElementalComposition != null ? mElementalComposition.getChemicalFormula() : null); 695 } 696 697 return mChemicalFormula; 698 } 699 700 //-------------------------------------------------------------------------- 701 /** 702 Returns a chemical formula String like 'Câ‚…Hâ‚â‚NO'. If carbon is present, it 703 is listed first followed by the other elements in ascending mass order. 704 Symbols for isotopes are enclosed in square brackets such as '[²H]â‚‚O' 705 for deuterated water. 706 @return the chemical formula string 707 */ 708 public String getChemicalFormulaWithSubscripts() 709 { 710 return mElementalComposition != null ? mElementalComposition.getChemicalFormulaWithSubscripts() : null; 711 } 712 713 714 //-------------------------------------------------------------------------- 715 public void clearCalculatedProperties() 716 { 717 if (! mMonoisotopicMassIsUserSet) mMonoisotopicMass = null; 718 if (! mAverageMassIsUserSet) mAverageMass = null; 719 mHashCode = null; 720 mChemicalFormula = null; 721 } 722 723 724 //-------------------------------------------------------------------------- 725 public Molecule addKa(IonizableGroup inValue) 726 { 727 return addKa(inValue, 1); 728 } 729 730 //-------------------------------------------------------------------------- 731 public Molecule addKa(IonizableGroup inValue, int inCount) 732 { 733 if (isLocked()) throw new UnmodifyableObjectException(name() + " is locked and cannot be modified!"); 734 735 if (null == mKas) 736 { 737 mKas = new ArrayList<>(3); 738 } 739 740 for (int i = 0; i < inCount; i++) 741 { 742 mKas.add(inValue); 743 } 744 745 return this; 746 } 747 748 //-------------------------------------------------------------------------- 749 /** 750 Returns a List of IonizableGroup objects. 751 @return List of IonizableGroups 752 */ 753 public List<IonizableGroup> getKas() 754 { 755 return mKas; 756 } 757 758 //-------------------------------------------------------------------------- 759 public Double getPhysicalProperty(PhysicalProperty inProperty) 760 { 761 Double value = null; 762 763 if (mPropertyMap != null) 764 { 765 value = mPropertyMap.get(inProperty); 766 } 767 768 return value; 769 } 770 771 //-------------------------------------------------------------------------- 772 public Molecule setPhysicalProperty(PhysicalProperty inProperty, Double inValue) 773 { 774 if (null == mPropertyMap) 775 { 776 mPropertyMap = new HashMap<>(5); 777 } 778 779 mPropertyMap.put(inProperty, inValue); 780 781 return this; 782 } 783 784 785 //-------------------------------------------------------------------------- 786 public void setAttribute(String inName, Object inValue) 787 { 788 getOrInitAttributeMgr().setAttribute(inName, inValue); 789 } 790 791 //-------------------------------------------------------------------------- 792 public boolean hasAttributes() 793 { 794 return mAttributeMgr != null && mAttributeMgr.hasAttributes(); 795 } 796 797 //-------------------------------------------------------------------------- 798 public boolean hasAttribute(String inName) 799 { 800 return mAttributeMgr != null && getOrInitAttributeMgr().hasAttribute(inName); 801 } 802 803 //-------------------------------------------------------------------------- 804 public Object getAttribute(String inName) 805 { 806 return getOrInitAttributeMgr().getAttribute(inName); 807 } 808 809 //-------------------------------------------------------------------------- 810 public Collection<String> getAttributeNames() 811 { 812 return getOrInitAttributeMgr().getAttributeNames(); 813 } 814 815 //-------------------------------------------------------------------------- 816 public void clearAttributes() 817 { 818 if (mAttributeMgr != null) 819 { 820 mAttributeMgr.clearAttributes(); 821 } 822 } 823 824 //-------------------------------------------------------------------------- 825 public Object removeAttribute(String inName) 826 { 827 Object attr = null; 828 if (mAttributeMgr != null) 829 { 830 attr = getOrInitAttributeMgr().removeAttribute(inName); 831 } 832 833 return attr; 834 } 835 836 //########################################################################## 837 // PROTECTED METHODS 838 //########################################################################## 839 840 //-------------------------------------------------------------------------- 841 public XMLNode toXMLNode() 842 { 843 XMLNode node = new XMLTag(HfgBioXML.MOL_TAG); 844 845 if (StringUtil.isSet(name())) node.setAttribute(HfgBioXML.NAME_ATT, name()); 846 if (getMonoisotopicMass() != null) node.setAttribute(HfgBioXML.MONO_MASS_ATT, 847 formatMassString(getMonoisotopicMass())); 848 if (getAverageMass() != null) node.setAttribute(HfgBioXML.AVG_MASS_ATT, 849 formatMassString(getAverageMass())); 850 // TODO: Add an organic mass tag? 851 852 Map<Element, Float> elementalCompositionMap = getElementalComposition(); 853 if (CollectionUtil.hasValues(elementalCompositionMap)) 854 { 855 XMLNode elementalCompTag = new XMLTag(HfgBioXML.ELEMENTAL_COMP_TAG); 856 node.addSubtag(elementalCompTag); 857 for (Map.Entry<Element, Float> entry : elementalCompositionMap.entrySet()) 858 { 859 elementalCompTag.setAttribute(entry.getKey().getName(), entry.getValue()); 860 } 861 } 862 863 if (CollectionUtil.hasValues(getKas())) 864 { 865 XMLNode kasTag = new XMLTag(HfgBioXML.KAS_TAG); 866 node.addSubtag(kasTag); 867 for (IonizableGroup grp : getKas()) 868 { 869 kasTag.addSubtag(grp.toXMLNode()); 870 } 871 } 872 873 if (mAttributeMgr != null) 874 { 875 node.addSubtag(mAttributeMgr.toXMLNode()); 876 } 877 878 return node; 879 } 880 881 //-------------------------------------------------------------------------- 882 protected void calculateMassFromElementalComposition() 883 { 884 Map<Element, Float> elementalCompositionMap = getElementalComposition(); 885 886 if (elementalCompositionMap != null) 887 { 888 double mono = 0.0; 889 double avg = 0.0; 890 double organicAvg = 0.0; 891 892 for (Element element : elementalCompositionMap.keySet()) 893 { 894 float count = elementalCompositionMap.get(element); 895 mono += count * element.getMonoisotopicMass(); 896 897 Double elementalAvgMass = element.getAverageMass(); 898 avg += count * (elementalAvgMass != null ? elementalAvgMass : element.getMonoisotopicMass()); // If we don't have an avg. mass for the element, use monoisotopic 899 900 Double elementalOrgAvgMass = element.getOrganicAverageMass(); 901 organicAvg += count * (elementalOrgAvgMass != null ? elementalOrgAvgMass : elementalAvgMass != null ? elementalAvgMass : element.getMonoisotopicMass()); 902 } 903 904 if (! mMonoisotopicMassIsUserSet) mMonoisotopicMass = mono; 905 if (! mAverageMassIsUserSet) mAverageMass = avg; 906 if (! mOrganicAverageMassIsUserSet) mOrganicAverageMass = organicAvg; 907 } 908 } 909 910 //-------------------------------------------------------------------------- 911 protected boolean massesAreUserSet() 912 { 913 return (mMonoisotopicMassIsUserSet || mAverageMassIsUserSet); 914 } 915 916 //-------------------------------------------------------------------------- 917 protected static String formatMassString(double inValue) 918 { 919 String massString = inValue + ""; 920 int index = massString.indexOf("."); 921 922 // More than 6 decimal places? Round to 6. 923 if (index > 0 924 && massString.length() - index > 6) 925 { 926 massString = String.format("%.6f", inValue); 927 } 928 return massString; 929 } 930 931 //########################################################################## 932 // PRIVATE METHODS 933 //########################################################################## 934 935 //-------------------------------------------------------------------------- 936 private AttributeMgr getOrInitAttributeMgr() 937 { 938 if (null == mAttributeMgr) 939 { 940 mAttributeMgr = new AttributeMgr(); 941 } 942 943 return mAttributeMgr; 944 } 945 946 //--------------------------------------------------------------------------- 947 // Used when parsing chemical formulas 948 private static Character convertSubscriptChar(char inChar) 949 { 950 Character subscriptChar = null; 951 switch (inChar) 952 { 953 case '\u2080': 954 subscriptChar = '0'; 955 break; 956 case '\u2081': 957 subscriptChar = '1'; 958 break; 959 case '\u2082': 960 subscriptChar = '2'; 961 break; 962 case '\u2083': 963 subscriptChar = '3'; 964 break; 965 case '\u2084': 966 subscriptChar = '4'; 967 break; 968 case '\u2085': 969 subscriptChar = '5'; 970 break; 971 case '\u2086': 972 subscriptChar = '6'; 973 break; 974 case '\u2087': 975 subscriptChar = '7'; 976 break; 977 case '\u2088': 978 subscriptChar = '8'; 979 break; 980 case '\u2089': 981 subscriptChar = '9'; 982 break; 983 } 984 985 return subscriptChar; 986 } 987 988 //--------------------------------------------------------------------------- 989 // Used when parsing isotopes in chemical formulas 990 private static Character convertSuperscriptChar(char inChar) 991 { 992 Character superscriptChar = null; 993 switch (inChar) 994 { 995 case '\u207B': 996 superscriptChar = '-'; 997 break; 998 case '\u2070': 999 superscriptChar = '0'; 1000 break; 1001 case 0xB9: 1002 superscriptChar = '1'; 1003 break; 1004 case 0xB2: 1005 superscriptChar = '2'; 1006 break; 1007 case 0xB3: 1008 superscriptChar = '3'; 1009 break; 1010 case '\u2074': 1011 superscriptChar = '4'; 1012 break; 1013 case '\u2075': 1014 superscriptChar = '5'; 1015 break; 1016 case '\u2076': 1017 superscriptChar = '6'; 1018 break; 1019 case '\u2077': 1020 superscriptChar = '7'; 1021 break; 1022 case '\u2078': 1023 superscriptChar = '8'; 1024 break; 1025 case '\u2079': 1026 superscriptChar = '9'; 1027 break; 1028 } 1029 1030 return superscriptChar; 1031 } 1032 1033 //########################################################################## 1034 // PRIVATE CLASS 1035 //########################################################################## 1036 1037 1038 private class FormulaSubBlock 1039 { 1040 private StringBuilder mString = new StringBuilder(); 1041 private int mCount = 1; 1042 private char mBracketType; 1043 private boolean mClosed = false; 1044 List<FormulaSubBlock> mSubBlocks; 1045 1046 //----------------------------------------------------------------------- 1047 public String toString() 1048 { 1049 return mString.toString(); 1050 } 1051 1052 //----------------------------------------------------------------------- 1053 public void append(char inChar) 1054 { 1055 mString.append(inChar); 1056 } 1057 1058 //----------------------------------------------------------------------- 1059 public void setCount(int inValue) 1060 { 1061 mCount = inValue; 1062 } 1063 1064 //----------------------------------------------------------------------- 1065 public FormulaSubBlock setBracketType(char inValue) 1066 { 1067 mBracketType = inValue; 1068 return this; 1069 } 1070 1071 //----------------------------------------------------------------------- 1072 public void close() 1073 { 1074 mClosed = true; 1075 } 1076 1077 //----------------------------------------------------------------------- 1078 public boolean isClosed() 1079 { 1080 return mClosed; 1081 } 1082 1083 //----------------------------------------------------------------------- 1084 public void addSubBlock(FormulaSubBlock inValue) 1085 { 1086 if (null == mSubBlocks) 1087 { 1088 mSubBlocks = new ArrayList<>(4); 1089 } 1090 1091 mSubBlocks.add(inValue); 1092 } 1093 1094 //----------------------------------------------------------------------- 1095 public Map<Element, Float> getChemicalComposition() 1096 { 1097 Map<Element, Float> chemicalCompositionMap = getChemicalComposition(mString.toString()); 1098 1099 if (CollectionUtil.hasValues(mSubBlocks)) 1100 { 1101 for (FormulaSubBlock subBlock : mSubBlocks) 1102 { 1103 Map<Element, Float> subBlockCompositionMap = subBlock.getChemicalComposition(); 1104 for (Element element : subBlockCompositionMap.keySet()) 1105 { 1106 float updatedCount = subBlockCompositionMap.get(element); 1107 1108 Float existingCount = chemicalCompositionMap.get(element); 1109 if (existingCount != null) 1110 { 1111 updatedCount += existingCount; 1112 } 1113 1114 chemicalCompositionMap.put(element, updatedCount); 1115 } 1116 } 1117 } 1118 1119 return chemicalCompositionMap; 1120 } 1121 1122 //----------------------------------------------------------------------- 1123 private Map<Element, Float> getChemicalComposition(String inString) 1124 { 1125 Map<Element, Float> chemicalCompositionMap = new HashMap<>(10); 1126 1127 int i = 0; 1128 char prevChar = ' '; 1129 while (i < inString.length()) 1130 { 1131 char currentChar = inString.charAt(i); 1132 1133 if (currentChar == ' ' // Skip whitespace 1134 || currentChar == ':' // Skip bond notation 1135 || (currentChar == '-' && ! Character.isDigit(prevChar)) // Skip linear formula single bond notation 1136 || currentChar == '−' // Skip linear formula single bond notation 1137 || currentChar == '=' // Skip linear formula double bond notation 1138 || currentChar == '≡' // Skip linear formula triple bond notation 1139 || currentChar == '@') // Skip trapped atom notation 1140 { 1141 i++; 1142 continue; 1143 } 1144 1145 if (mBracketType == '[' 1146 && 0 == i 1147 && (Character.isDigit(currentChar) 1148 || CharUtil.isSuperscript(currentChar))) 1149 { 1150 // Isotope 1151 1152 String massNumString = ""; 1153 Character convertedSuperscriptChar = null; 1154 while (i < inString.length()) 1155 { 1156 char theChar = inString.charAt(i); 1157 if (Character.isDigit(theChar) 1158 || (convertedSuperscriptChar = convertSuperscriptChar(theChar)) != null) 1159 { 1160 massNumString += (convertedSuperscriptChar != null ? convertedSuperscriptChar : theChar); 1161 i++; 1162 } 1163 else 1164 { 1165 break; 1166 } 1167 } 1168 1169 Element element = null; 1170 if (i < inString.length() - 1) 1171 { 1172 element = Element.valueOf(inString.substring(i, i + 2)); 1173 } 1174 1175 if (element != null) 1176 { 1177 i += 2; 1178 } 1179 else 1180 { 1181 element = Element.valueOf(inString.substring(i, i + 1)); 1182 if (element != null) 1183 { 1184 i++; 1185 } 1186 else 1187 { 1188 throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + "!"); 1189 } 1190 } 1191 1192 Isotope isotope = Isotope.valueOf(element, Integer.parseInt(massNumString)); 1193 1194 float updatedCount = mCount; 1195 1196 Float existingCount = chemicalCompositionMap.get(isotope); 1197 if (existingCount != null) 1198 { 1199 updatedCount += existingCount; 1200 } 1201 1202 chemicalCompositionMap.put(isotope, updatedCount); 1203 } 1204 else if (sSaltIndicatorsInChemicalFormulas.contains(currentChar)) 1205 { 1206 // molecule of crystallization 1207 i++; 1208 String countString = ""; 1209 while (i < inString.length()) 1210 { 1211 char countChar = inString.charAt(i); 1212 1213 if (countChar != ' ') // Skip whitespace 1214 { 1215 if (Character.isDigit(countChar)) 1216 { 1217 countString += countChar; 1218 } 1219 else 1220 { 1221 break; 1222 } 1223 } 1224 1225 i++; 1226 } 1227 1228 float count = StringUtil.isSet(countString) ? Float.parseFloat(countString) : 1; 1229 1230 Map<Element, Float> crystallizationChemicalCompositionMap = getChemicalComposition(inString.substring(i)); 1231 for (Element element : crystallizationChemicalCompositionMap.keySet()) 1232 { 1233 float updatedCount = crystallizationChemicalCompositionMap.get(element) * count * mCount; 1234 1235 Float existingCount = chemicalCompositionMap.get(element); 1236 if (existingCount != null) 1237 { 1238 updatedCount += existingCount; 1239 } 1240 1241 chemicalCompositionMap.put(element, updatedCount); 1242 } 1243 1244 // There shouldn't be anything after the crystallization molecule 1245 break; 1246 } 1247 else 1248 { 1249 Element element = null; 1250 if (i < inString.length() - 1) 1251 { 1252 element = Element.valueOf(inString.substring(i, i + 2)); 1253 } 1254 1255 if (element != null) 1256 { 1257 i += 2; 1258 } 1259 else 1260 { 1261 element = Element.valueOf(inString.substring(i, i + 1)); 1262 if (element != null) 1263 { 1264 i++; 1265 } 1266 else 1267 { 1268 throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + "!"); 1269 } 1270 } 1271 1272 String countString = ""; 1273 Character convertedSubscriptChar = null; 1274 while (i < inString.length()) 1275 { 1276 char countChar = inString.charAt(i); 1277 if (Character.isDigit(countChar) 1278 || (convertedSubscriptChar = convertSubscriptChar(countChar)) != null) 1279 { 1280 countString += (convertedSubscriptChar != null ? convertedSubscriptChar : countChar); 1281 i++; 1282 } 1283 else if ('-' == countChar) 1284 { 1285 throw new ChemicalFormulaParseException("Problem parsing elements from " + StringUtil.singleQuote(inString) + " at char " + (i + 1) + ": Counts can't be ranges!"); 1286 } 1287 else 1288 { 1289 break; 1290 } 1291 } 1292 1293 float updatedCount = mCount * (StringUtil.isSet(countString) ? Float.parseFloat(countString) : 1); 1294 1295 Float existingCount = chemicalCompositionMap.get(element); 1296 if (existingCount != null) 1297 { 1298 updatedCount += existingCount; 1299 } 1300 1301 chemicalCompositionMap.put(element, updatedCount); 1302 } 1303 1304 prevChar = currentChar; 1305 1306 } 1307 1308 return chemicalCompositionMap; 1309 } 1310 } 1311}