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}