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}