001package com.hfg.bio.seq.format.abi;
002
003
004import java.awt.Color;
005import java.awt.Font;
006import java.awt.Point;
007import java.awt.font.FontRenderContext;
008import java.awt.geom.AffineTransform;
009import java.awt.geom.Point2D;
010import java.awt.geom.Rectangle2D;
011import java.io.File;
012import java.io.IOException;
013import java.io.RandomAccessFile;
014import java.nio.ByteOrder;
015import java.util.ArrayList;
016import java.util.Calendar;
017import java.util.Date;
018import java.util.GregorianCalendar;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022
023import com.hfg.bio.Nucleotide;
024import com.hfg.bio.seq.NucleicAcid;
025import com.hfg.bio.seq.SeqQualityScores;
026import com.hfg.bio.seq.format.SeqIOException;
027import com.hfg.graphics.ColorUtil;
028import com.hfg.graphics.TextUtil;
029import com.hfg.html.attribute.HTMLColor;
030import com.hfg.svg.SVG;
031import com.hfg.svg.SvgGroup;
032import com.hfg.svg.SvgPath;
033import com.hfg.svg.SvgText;
034import com.hfg.svg.path.SvgPathLineToCmd;
035import com.hfg.svg.path.SvgPathMoveToCmd;
036import com.hfg.util.ByteUtil;
037import com.hfg.util.StringUtil;
038import com.hfg.util.io.ByteSource;
039
040//------------------------------------------------------------------------------
041/**
042 * Extracts data from ABIF (ab1) format sequencing chromatographic trace files.
043 *
044 * @author J. Alex Taylor, hairyfatguy.com
045 */
046//------------------------------------------------------------------------------
047// com.hfg XML/HTML Coding Library
048//
049// This library is free software; you can redistribute it and/or
050// modify it under the terms of the GNU Lesser General Public
051// License as published by the Free Software Foundation; either
052// version 2.1 of the License, or (at your option) any later version.
053//
054// This library is distributed in the hope that it will be useful,
055// but WITHOUT ANY WARRANTY; without even the implied warranty of
056// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
057// Lesser General Public License for more details.
058//
059// You should have received a copy of the GNU Lesser General Public
060// License along with this library; if not, write to the Free Software
061// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
062//
063// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
064// jataylor@hairyfatguy.com
065//------------------------------------------------------------------------------
066// See http://www6.appliedbiosystems.com/support/software_community/ABIF_File_Format.pdf
067
068public class ABIF
069{
070   private int     mVersionNum;
071   private String  mInstrumentClass;
072   private String  mInstrumentFamily;
073   private String  mInstrumentName;
074   private String  mMachineName;
075   private String  mInstrumentParameters;
076   private String  mBaseOrder;
077   private Integer mLane;
078   private String  mBasecallerSeq;
079   private short[] mBasecallerQualityScores;
080   private short[] mBasecallerPeakLocations;
081   private String  mUserSeq;
082   private short[] mUserQualityScores;
083   private short[] mUserPeakLocations;
084   private Integer mNumTraceDataPoints;
085   private Short   mMaxTraceDataValue;
086   private Map<Character, short[]> mTraceDataMap = new HashMap<>(4);
087   private String  mSampleName;
088   private String  mSampleComment;
089   private String  mSampleTrackingID;
090   private String  mQC_Warnings;
091   private String  mQC_Errors;
092   private Long    mQV20_Score;
093   private String  mQV20_Status;
094   private Date    mRunDate;
095   private Integer mMaxQualityValue;
096
097   private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
098   private static FontRenderContext sFRC = new FontRenderContext(new AffineTransform(), true, true);
099
100   // These represent the file tags that we are currently parsing
101   private enum FileTag
102   {
103      CMNT,    // Sample comment
104      DATA,    // instances 9-12: short[] holding analyzed color data
105      FWO_,    // Sequencing Analysis Filter wheel order. Fixed for 3500 at "GATC"
106      HCFG,    // 1st instance: The instrument class. All upper case, no spaces.
107               // 2nd instance: The instrument family. All upper case, no spaces.
108               // 3rd instance: The official instrument name. Mixed case, minus any special formatting.
109               // 4th instance: Instrument parameters. Contains key-value pairs of instrument configuration
110               //               information, separated by semicolons. Four parameters are included initially:
111               //               UnitID=<UNITID number>, CPUBoard=<board type>, ArraySize=<# of capillaries>,
112               //               SerialNumber=<Instrument Serial#>.
113      LANE,    // Sample's lane or capillary number
114      LIMS,    // Sample tracking ID
115      MCHN,    // Machine Name.
116      PBAS,    // 1st instance: Array of sequence characters edited by user
117               // 2nd instance: Array of sequence characters as called by Basecaller
118      PCON,    // 1st instance: Array of quality values (0-255) as edited by user
119               // 2nd instance: Array of quality values (0-255) as called by Basecaller
120      phQL,    // Maximum quality value
121      PLOC,    // 1st instance: Array of peak locations edited by user
122               // 2nd instance: Array of peak locations as called by Basecaller
123      QcRs,    // 1st instance: QC warnings, a concatenated comma-separated string      (3500/3500xl specific?)
124               // 2nd instance: QC errors, a concatenated comma-separated string        (3500/3500xl specific?)
125      QV20,    // 1st instance: QV20+ value                                             (3500/3500xl specific?)
126               // 2nd instance: One of 'Pass', 'Fail', or 'Check'                       (3500/3500xl specific?)
127      RUND,    // 1st instance: Run started date
128               // 2nd instance: Run stopped date
129      RUNT,    // 1st instance: Run started time
130               // 2nd instance: Run stopped time
131      SMPL     // Sequencing analysis sample name
132   }
133
134   //###########################################################################
135   // CONSTRUCTORS
136   //###########################################################################
137
138   //---------------------------------------------------------------------------
139   public ABIF(File inFile)
140      throws IOException
141   {
142      if (! inFile.exists())
143      {
144         throw new IOException("The specified ABIF file " + StringUtil.singleQuote(inFile.getPath()) + " doesn't exist!");
145      }
146
147      if (! inFile.canRead())
148      {
149         throw new IOException("You do not have read access to the specified ABIF file " + StringUtil.singleQuote(inFile.getPath()) + "!");
150      }
151
152      RandomAccessFile randomAccessFile = null;
153      try
154      {
155         randomAccessFile = new RandomAccessFile(inFile,  "r");
156         init(new ByteSource(randomAccessFile));
157      }
158      finally
159      {
160         randomAccessFile.close();
161      }
162   }
163
164   //---------------------------------------------------------------------------
165   public ABIF(byte[] inBytes)
166      throws IOException
167   {
168      init(new ByteSource(inBytes));
169   }
170
171   //###########################################################################
172   // PUBLIC METHODS
173   //###########################################################################
174
175   //---------------------------------------------------------------------------
176   public int getABIF_FormatVersionNum()
177   {
178      return mVersionNum;
179   }
180
181   //---------------------------------------------------------------------------
182   public Integer getLane()
183   {
184      return mLane;
185   }
186
187   //---------------------------------------------------------------------------
188   public String getInstrumentClass()
189   {
190      return mInstrumentClass;
191   }
192
193   //---------------------------------------------------------------------------
194   public String getInstrumentFamily()
195   {
196      return mInstrumentFamily;
197   }
198
199   //---------------------------------------------------------------------------
200   public String getInstrumentName()
201   {
202      return mInstrumentName;
203   }
204
205   //---------------------------------------------------------------------------
206   public String getMachineName()
207   {
208      return mMachineName;
209   }
210
211   //---------------------------------------------------------------------------
212   public Date getRunDate()
213   {
214      return mRunDate;
215   }
216
217   //---------------------------------------------------------------------------
218   public String getSampleName()
219   {
220      return mSampleName;
221   }
222
223   //---------------------------------------------------------------------------
224   public String getSampleTrackingID()
225   {
226      return mSampleTrackingID;
227   }
228
229   //---------------------------------------------------------------------------
230   public String getSampleComment()
231   {
232      return mSampleComment;
233   }
234
235   //---------------------------------------------------------------------------
236   public String getBasecallerSeq()
237   {
238      return mBasecallerSeq;
239   }
240
241   //---------------------------------------------------------------------------
242   public short[] getBasecallerPeakLocations()
243   {
244      return mBasecallerPeakLocations;
245   }
246
247   //---------------------------------------------------------------------------
248   public short[] getBasecallerQualityScores()
249   {
250      return mBasecallerQualityScores;
251   }
252
253   //---------------------------------------------------------------------------
254   public String getUserSeq()
255   {
256      return mUserSeq;
257   }
258
259   //---------------------------------------------------------------------------
260   public short[] getUserQualityScores()
261   {
262      return mUserQualityScores;
263   }
264
265   //---------------------------------------------------------------------------
266   public Integer getMaxQualityScore()
267   {
268      return mMaxQualityValue;
269   }
270
271   //---------------------------------------------------------------------------
272   public short[] getUserPeakLocations()
273   {
274      return mUserPeakLocations;
275   }
276
277   //---------------------------------------------------------------------------
278   public short[] getTraceValues(Nucleotide inNucleotide)
279   {
280      return mTraceDataMap.get(Character.toUpperCase(inNucleotide.getOneLetterCode()));
281   }
282
283   //---------------------------------------------------------------------------
284   /**
285    3500/3500xl specific: Returns any QC warnings as a concatenated comma-separated string.
286    @return QC warnings
287    */
288   public String getQC_Warnings()
289   {
290      return mQC_Warnings;
291   }
292
293   //---------------------------------------------------------------------------
294   /**
295    3500/3500xl specific: Returns any QC errors as a concatenated comma-separated string.
296    @return QC errors or null
297    */
298   public String getQC_Errors()
299   {
300      return mQC_Errors;
301   }
302
303   //---------------------------------------------------------------------------
304   /**
305    3500/3500xl specific: Returns the QV20+ value.
306    @return QV20+ score or null
307    */
308   public Long getQV20_Score()
309   {
310      return mQV20_Score;
311   }
312
313   //---------------------------------------------------------------------------
314   /**
315    3500/3500xl specific: Returns the QV20 status as one of 'Pass', 'Fail', or 'Check'.
316    @return QV20 status or null
317    */
318   public String getQV20_Status()
319   {
320      return mQV20_Status;
321   }
322
323   //---------------------------------------------------------------------------
324   public NucleicAcid toNucleicAcid()
325   {
326      NucleicAcid seq = new NucleicAcid().setID(getSampleName());
327      if (StringUtil.isSet(getUserSeq()))
328      {
329         seq.setSequence(StringUtil.isSet(getUserSeq()) ? getUserSeq() : getBasecallerSeq());
330      }
331
332      short[] qualityScores = null;
333      if (getUserQualityScores() != null)
334      {
335         qualityScores = getUserQualityScores();
336      }
337      else if (getBasecallerQualityScores() != null)
338      {
339         qualityScores = getBasecallerQualityScores();
340      }
341
342      if (qualityScores != null)
343      {
344         seq.setSeqQualityScores(new SeqQualityScores(qualityScores));
345      }
346
347      return seq;
348   }
349
350   //--------------------------------------------------------------------------
351   /**
352    Returns the largest trace intensity value - helpful for scaling.
353    * @return the largest trace intensity value
354    */
355   public short getMaxTraceValue()
356   {
357      if (null == mMaxTraceDataValue)
358      {
359         short maxValue = 0;
360
361         for (short[] colorDataValues : mTraceDataMap.values())
362         {
363            for (short value : colorDataValues)
364            {
365               if (value > maxValue)
366               {
367                  maxValue = value;
368               }
369            }
370         }
371
372         mMaxTraceDataValue = maxValue;
373      }
374
375      return mMaxTraceDataValue;
376   }
377
378   //--------------------------------------------------------------------------
379   public SVG toSVG()
380   {
381      int height = 400;
382      int width = mNumTraceDataPoints * 2;
383      Font font = Font.decode("Arial-PLAIN-9");
384      int marginSize = 20;
385
386      // Determine the scale.
387      double xScalingFactor = width / (float) mNumTraceDataPoints;
388      double yScalingFactor = height / (float) getMaxTraceValue();
389      double yQualityScalingFactor = height / (float) getMaxQualityScore();
390
391      int scaledMaxY = (int) (getMaxTraceValue() * yScalingFactor);
392      int scaledMaxQualityY = (int) (getMaxQualityScore() * yQualityScalingFactor);
393
394      int lineHeight = (int) TextUtil.getStringRect("A", font).getHeight();
395
396
397      short[] peakLocations = getUserPeakLocations();
398      if (null == peakLocations)
399      {
400         peakLocations = getBasecallerPeakLocations();
401      }
402
403      SVG svg = new SVG();
404      svg.setFont(font);
405
406
407      // Display the quality scores in the background
408      short[] qualityScores = getUserQualityScores();
409      if (null == qualityScores)
410      {
411         qualityScores = getBasecallerQualityScores();
412      }
413
414      if (qualityScores != null)
415      {
416         SvgGroup group = svg.addGroup().setClass("quality");
417         group.addStyle("stroke:#" + ColorUtil.colorToHex(HTMLColor.DARK_GRAY));
418
419         SvgPath path = group.addPath().addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Float(marginSize, height + marginSize)));
420         List<Float> lineToValues = new ArrayList<>(peakLocations.length * 4);
421         float prevPeakX = 0;
422         for (int i = 0; i < peakLocations.length; i++)
423         {
424            float scaledPeakX = marginSize + (float) (peakLocations[i] * xScalingFactor);
425            float nextScaledPeakX = (i < qualityScores.length - 1 ? marginSize + (float) (peakLocations[i + 1] * xScalingFactor) : width - marginSize);
426
427            float leftX = (i > 0 ? scaledPeakX - (scaledPeakX - prevPeakX)/2f : marginSize);
428            float rightX = (i < qualityScores.length ? scaledPeakX + (nextScaledPeakX - scaledPeakX)/2f : width - marginSize);
429            float y = marginSize + scaledMaxQualityY - (int) (qualityScores[i] * yQualityScalingFactor);
430
431            lineToValues.add(leftX);
432            lineToValues.add(y);
433
434            lineToValues.add(rightX);
435            lineToValues.add(y);
436
437            prevPeakX = scaledPeakX;
438         }
439         lineToValues.add(width - (float) marginSize);
440         lineToValues.add(marginSize + (float) scaledMaxQualityY);
441
442         path.addPathCommand(new SvgPathLineToCmd().setRawNumbers(lineToValues));
443         path.setFill(HTMLColor.LIGHT_GRAY).addStyle("fill-opacity:0.4; stroke-opacity:0.2");
444      }
445
446      // Display the nucleotide traces
447      for (Character nucleotide : new Character[] {'A', 'C', 'G', 'T'})
448      {
449         short[] traceValues = mTraceDataMap.get(nucleotide);
450
451         SvgGroup group = svg.addGroup().setClass(nucleotide + "");
452         group.addStyle("stroke:#" + ColorUtil.colorToHex(getColorForNucleotide(nucleotide)));
453
454         SvgPath path = group.addPath().addPathCommand(new SvgPathMoveToCmd().addPoint(new Point2D.Float(marginSize, height - marginSize)));
455         List<Float> lineToValues = new ArrayList<>(traceValues.length * 2);
456         for (int i = 0; i < traceValues.length; i++)
457         {
458            float x = marginSize + (int) (i * xScalingFactor);
459            float y = marginSize + scaledMaxY - (int) (traceValues[i] * yScalingFactor);
460
461            lineToValues.add(x);
462            lineToValues.add(y);
463         }
464
465         path.addPathCommand(new SvgPathLineToCmd().setRawNumbers(lineToValues));
466         path.setFill(null);
467      }
468
469      // Display the called (or user-specified) sequence along the top
470      String sequence = getUserSeq();
471      if (null == sequence)
472      {
473         sequence = getBasecallerSeq();
474      }
475
476      if (StringUtil.isSet(sequence)
477            && peakLocations != null)
478      {
479         SvgGroup group = svg.addGroup().setClass("sequence");
480
481         // Used to better calculate text placement
482         Rectangle2D textBoundBox = font.getStringBounds("A", 0, "A".length(), sFRC);
483
484         for (int i = 0; i < peakLocations.length; i++)
485         {
486            short peakLocation = peakLocations[i];
487            char nucleotide = sequence.charAt(i);
488
489            float x = marginSize + (int) ((peakLocation * xScalingFactor) - (textBoundBox.getWidth() / 2));
490            float y = marginSize;
491            SvgText label = group.addText(nucleotide + "", font, new Point.Float(x, y));
492
493            label.setFill(getColorForNucleotide(nucleotide));
494
495            if ((i+1)%10 == 0)
496            {
497               x = marginSize + (int) (peakLocation * xScalingFactor);
498               y = marginSize + lineHeight;
499                           
500               group.addText((i + 1) + "", font, new Point.Float(x, y)).setFill(Color.LIGHT_GRAY);
501            }
502         }
503      }
504
505
506      return svg;
507   }
508
509   //--------------------------------------------------------------------------
510   Color getColorForNucleotide(Character inNucleotide)
511   {
512      Color color;
513      switch (inNucleotide)
514      {
515         case 'A':
516            color = HTMLColor.GREEN;
517            break;
518         case 'C':
519            color = HTMLColor.BLUE;
520            break;
521         case 'G':
522            color = HTMLColor.BLACK;
523            break;
524         case 'T':
525            color = HTMLColor.RED;
526            break;
527         default:
528            color = HTMLColor.DARK_GRAY;
529      }
530
531      return color;
532   }
533
534   //###########################################################################
535   // PRIVATE METHODS
536   //###########################################################################
537
538   //---------------------------------------------------------------------------
539   private void init(ByteSource inByteSource)
540         throws IOException
541   {
542      DirectoryEntry header = readHeader(inByteSource);
543
544      // Skip to the directory entries
545      inByteSource.seek(header.getDataOffset());
546
547      // Read directory entries (located at the end of the file)
548      List<DirectoryEntry> dirEntries = new ArrayList<>(header.getNumElements());
549      for (int i = 0; i < header.getNumElements(); i++)
550      {
551         dirEntries.add(new DirectoryEntry(inByteSource));
552      }
553
554      extractData(inByteSource, dirEntries);
555   }
556
557   //---------------------------------------------------------------------------
558   private DirectoryEntry readHeader(ByteSource inByteSource)
559      throws IOException
560   {
561      byte[] fileSignature = new byte[4];
562      inByteSource.read(fileSignature);
563
564      if (! new String(fileSignature).equals("ABIF"))
565      {
566         throw new SeqIOException("ABIF file signature not found!");
567      }
568
569      mVersionNum = ByteUtil.get2ByteInt(inByteSource, mByteOrder);
570
571      // Read the dir entry
572      DirectoryEntry dirEntry = new DirectoryEntry(inByteSource);
573
574      // The dataSize should be exactly the size required for the entries (numElements x elementSize)
575      // TODO: Add sanity check
576
577      return dirEntry;
578   }
579
580   //---------------------------------------------------------------------------
581   private void extractData(ByteSource inByteSource, List<DirectoryEntry> inDirEntries)
582      throws IOException
583   {
584      // Create a temporary list for the trace value data
585      List<short[]> traceValueList = new ArrayList<>(4);
586      for (int i = 0; i < 4; i++)
587      {
588         traceValueList.add(null);
589      }
590
591      short maxQualityScore = 0;
592      
593      for (DirectoryEntry dirEntry : inDirEntries)
594      {
595         if (dirEntry.getTagName().equals(FileTag.CMNT.name()))
596         {
597            mSampleComment = getStringValueForDirectoryEntry(inByteSource, dirEntry);
598         }
599         else if (dirEntry.getTagName().equals(FileTag.DATA.name()))
600         {
601            if (dirEntry.getTagNum() >= 9
602                && dirEntry.getTagNum() <= 12)
603            {
604               inByteSource.seek(dirEntry.getDataOffset());
605               short[] traceValues = ByteUtil.getShortArray(inByteSource, dirEntry.getNumElements(), mByteOrder);
606
607               int index = dirEntry.getTagNum() - 9;
608
609               // Since we may not yet know the base order, store the trace values in an array temporarily;
610               traceValueList.set(index, traceValues);
611
612               if (null == mNumTraceDataPoints)
613               {
614                  mNumTraceDataPoints = traceValues.length;
615               }
616            }
617         }
618         else if (dirEntry.getTagName().equals(FileTag.FWO_.name()))
619         {
620            mBaseOrder = new String(ByteUtil.getBytesFromInt(dirEntry.getDataOffset()));
621         }
622         else if (dirEntry.getTagName().equals(FileTag.HCFG.name()))
623         {
624            String value = getStringValueForDirectoryEntry(inByteSource, dirEntry);
625
626            if (1 == dirEntry.getTagNum())
627            {
628               mInstrumentClass = value;
629            }
630            else if (2 == dirEntry.getTagNum())
631            {
632               mInstrumentFamily = value;
633            }
634            else if (3 == dirEntry.getTagNum())
635            {
636               mInstrumentName = value;
637            }
638            else if (4 == dirEntry.getTagNum())
639            {
640               mInstrumentParameters = value;
641            }
642         }
643         else if (dirEntry.getTagName().equals(FileTag.LANE.name()))
644         {
645            byte[] bytes = ByteUtil.getBytesFromInt(dirEntry.getDataOffset());
646            mLane = ByteUtil.get2ByteInt(bytes, mByteOrder);
647         }
648         else if (dirEntry.getTagName().equals(FileTag.LIMS.name()))
649         {
650            mSampleTrackingID = getStringValueForDirectoryEntry(inByteSource, dirEntry);
651         }
652         else if (dirEntry.getTagName().equals(FileTag.MCHN.name()))
653         {
654            mMachineName = getStringValueForDirectoryEntry(inByteSource, dirEntry);
655         }
656         else if (dirEntry.getTagName().equals(FileTag.PBAS.name()))
657         {
658            inByteSource.seek(dirEntry.getDataOffset());
659            String seqString = ByteUtil.getString(inByteSource, dirEntry.getDataSize());
660
661            if (1 == dirEntry.getTagNum())
662            {
663               mUserSeq = seqString;
664            }
665            else if (2 == dirEntry.getTagNum())
666            {
667               mBasecallerSeq = seqString;
668            }
669         }
670         else if (dirEntry.getTagName().equals(FileTag.PCON.name()))
671         {
672            inByteSource.seek(dirEntry.getDataOffset());
673
674            byte[] bytes = new byte[dirEntry.getDataSize()];
675            inByteSource.read(bytes);
676
677            // Converting the unsigned byte values to shorts for ease of handling
678            short[] qualityScores = new short[dirEntry.getDataSize()];
679            for (int i = 0; i < bytes.length; i++)
680            {
681               short qualityScore = (short) bytes[i];
682               qualityScores[i] = qualityScore;
683               
684               if (qualityScore > maxQualityScore)
685               {
686                  maxQualityScore = qualityScore;
687               }
688            }
689
690            if (1 == dirEntry.getTagNum())
691            {
692               mUserQualityScores = qualityScores;
693            }
694            else if (2 == dirEntry.getTagNum())
695            {
696               mBasecallerQualityScores = qualityScores;
697            }
698         }
699         else if (dirEntry.getTagName().equals(FileTag.phQL.name()))
700         {
701            mMaxQualityValue = (int) ByteUtil.getShort(ByteUtil.getBytesFromInt(dirEntry.getDataOffset()), 0, mByteOrder);
702         }
703         else if (dirEntry.getTagName().equals(FileTag.PLOC.name()))
704         {
705            inByteSource.seek(dirEntry.getDataOffset());
706            short[] peakLocations = ByteUtil.getShortArray(inByteSource, dirEntry.getNumElements(), mByteOrder);
707
708            if (1 == dirEntry.getTagNum())
709            {
710               mUserPeakLocations = peakLocations;
711            }
712            else if (2 == dirEntry.getTagNum())
713            {
714               mBasecallerPeakLocations = peakLocations;
715            }
716         }
717         else if (dirEntry.getTagName().equals(FileTag.QcRs.name()))
718         {
719            if (1 == dirEntry.getTagNum())
720            {
721               mQC_Warnings = getStringValueForDirectoryEntry(inByteSource, dirEntry);
722            }
723            else if (2 == dirEntry.getTagNum())
724            {
725               mQC_Errors = getStringValueForDirectoryEntry(inByteSource, dirEntry);
726            }
727         }
728         else if (dirEntry.getTagName().equals(FileTag.QV20.name()))
729         {
730            if (1 == dirEntry.getTagNum())
731            {
732               inByteSource.seek(dirEntry.getDataOffset());
733               mQV20_Score = ByteUtil.getLong(inByteSource, mByteOrder);
734            }
735            else if (2 == dirEntry.getTagNum())
736            {
737               mQV20_Status = getStringValueForDirectoryEntry(inByteSource, dirEntry);
738            }
739         }
740         else if (dirEntry.getTagName().equals(FileTag.RUND.name())
741                  && 1 == dirEntry.getTagNum())
742         {
743            byte[] bytes = ByteUtil.getBytesFromInt(dirEntry.getDataOffset());
744            int year = ByteUtil.get2ByteInt(bytes, 0, mByteOrder);
745            int month = ByteUtil.get1ByteInt(bytes, 2);
746            int day = ByteUtil.get1ByteInt(bytes, 3);
747
748            Calendar cal = new GregorianCalendar();
749            if (mRunDate != null)
750            {
751               cal.setTime(mRunDate);
752            }
753
754            cal.set(Calendar.YEAR, year);
755            cal.set(Calendar.MONTH, month - 1);
756            cal.set(Calendar.DAY_OF_MONTH, day);
757            mRunDate = cal.getTime();
758         }
759         else if (dirEntry.getTagName().equals(FileTag.RUNT.name())
760                  && 1 == dirEntry.getTagNum())
761         {
762            byte[] bytes = ByteUtil.getBytesFromInt(dirEntry.getDataOffset());
763            int hour = ByteUtil.get1ByteInt(bytes, 0);
764            int minute = ByteUtil.get1ByteInt(bytes, 1);
765            int second = ByteUtil.get1ByteInt(bytes, 2);
766            int hsecond = ByteUtil.get1ByteInt(bytes, 3);
767
768            Calendar cal = new GregorianCalendar();
769            if (mRunDate != null)
770            {
771               cal.setTime(mRunDate);
772            }
773
774            cal.set(Calendar.HOUR_OF_DAY, hour);
775            cal.set(Calendar.MINUTE, minute);
776            cal.set(Calendar.SECOND, second);
777            mRunDate = cal.getTime();
778         }
779         else if (dirEntry.getTagName().equals(FileTag.SMPL.name()))
780         {
781            mSampleName = getStringValueForDirectoryEntry(inByteSource, dirEntry);
782         }
783      }
784      
785      if (null == mMaxQualityValue)
786      {
787         mMaxQualityValue = (int) maxQualityScore;
788      }
789
790      // Now construct the trace data map
791      for (int i = 0; i < 4; i++)
792      {
793         mTraceDataMap.put(Character.toUpperCase(mBaseOrder.charAt(i)), traceValueList.get(i));
794      }
795   }
796
797   //---------------------------------------------------------------------------
798   private String getStringValueForDirectoryEntry(ByteSource inByteSource, DirectoryEntry inDirEntry)
799      throws IOException
800   {
801      String value;
802      if (inDirEntry.getDataSize() <= 4)
803      {
804         value = new String(ByteUtil.getBytesFromInt(inDirEntry.getDataOffset()));
805         int zeroIndex = value.indexOf(0);
806         if (zeroIndex >= 0)
807         {
808            value = value.substring(0, zeroIndex);
809         }
810      }
811      else
812      {
813         inByteSource.seek(inDirEntry.getDataOffset());
814         value = ByteUtil.getString(inByteSource, inDirEntry.getDataSize());
815      }
816
817      // Pascal type string with the lenght as the first byte?
818      if (18 == inDirEntry.getElementType()
819          && value.length() > 1)
820      {
821         value = value.substring(1);
822      }
823
824      return value;
825   }
826
827   //###########################################################################
828   // INNER CLASS
829   //###########################################################################
830
831   private class DirectoryEntry
832   {
833      private String mTagName;
834      private int    mTagNum;
835      private int    mElementType;
836      private int    mElementSize;
837      private int    mNumElements;
838      private int    mDataSize;
839      private int    mDataOffset;
840      private int    mDataHandle;
841
842      //------------------------------------------------------------------------
843      public DirectoryEntry(ByteSource inByteSource)
844         throws IOException
845      {
846         mTagName = ByteUtil.getString(inByteSource, 4);
847         mTagNum = ByteUtil.getInt(inByteSource, mByteOrder);
848         mElementType = ByteUtil.get2ByteInt(inByteSource, mByteOrder);
849         mElementSize = ByteUtil.get2ByteInt(inByteSource, mByteOrder);
850         mNumElements = ByteUtil.getInt(inByteSource, mByteOrder);
851         mDataSize = ByteUtil.getInt(inByteSource, mByteOrder);
852         mDataOffset = ByteUtil.getInt(inByteSource, mByteOrder);
853         mDataHandle = ByteUtil.getInt(inByteSource, mByteOrder);
854      }
855
856      //------------------------------------------------------------------------
857      public String getTagName()
858      {
859         return mTagName;
860      }
861
862      //------------------------------------------------------------------------
863      public int getTagNum()
864      {
865         return mTagNum;
866      }
867
868      //------------------------------------------------------------------------
869      public int getElementType()
870      {
871         return mElementType;
872      }
873
874      //------------------------------------------------------------------------
875      public int getNumElements()
876      {
877         return mNumElements;
878      }
879
880      //------------------------------------------------------------------------
881      public int getDataSize()
882      {
883         return mDataSize;
884      }
885
886      //------------------------------------------------------------------------
887      public int getDataOffset()
888      {
889         return mDataOffset;
890      }
891
892      //------------------------------------------------------------------------
893      @Override
894      public String toString()
895      {
896         return mTagName;
897      }
898
899   }
900}