public class AnalysisRunner implements Runnable {

	private SequenceFile file;
	private QCModule [] modules;
	private List<AnalysisListener> listeners = new ArrayList<AnalysisListener>();
	private int percentComplete = 0;
	
	public AnalysisRunner (SequenceFile file) {
		this.file = file;
	}
	
	public void addAnalysisListener (AnalysisListener l) {
		if (l != null && !listeners.contains(l)) {
			listeners.add(l);
		}
	}

	public void removeAnalysisListener (AnalysisListener l) {
		if (l != null && listeners.contains(l)) {
			listeners.remove(l);
		}
	}

	
	public void startAnalysis (QCModule [] modules) {
		this.modules = modules;
		for (int i=0;i<modules.length;i++) {
			modules[i].reset();
		}
		AnalysisQueue.getInstance().addToQueue(this);
	}

	public void run() {

		Iterator<AnalysisListener> i = listeners.iterator();
		while (i.hasNext()) {
			i.next().analysisStarted(file);
		}

		
		int seqCount = 0;
		while (file.hasNext()) {
			++seqCount;
			Sequence seq;
			try {
				seq = file.next();
			}
			catch (SequenceFormatException e) {
				i = listeners.iterator();
				while (i.hasNext()) {
					i.next().analysisExceptionReceived(file,e);
				}
				return;
			}
			
			for (int m=0;m<modules.length;m++) {
				if (seq.isFiltered() && modules[m].ignoreFilteredSequences()) continue;
				modules[m].processSequence(seq);
			}
			
			if (file.getPercentComplete() == percentComplete+5) {
			
				percentComplete = (int)file.getPercentComplete();
				
				i = listeners.iterator();
					while (i.hasNext()) {
						i.next().analysisUpdated(file,seqCount,percentComplete);
					}
					try {
						Thread.sleep(10);
					} 
					catch (InterruptedException e) {}
			}
		}
		
		i = listeners.iterator();
		while (i.hasNext()) {
			i.next().analysisComplete(file,modules);
		}

	}
	
}
 public class OfflineRunner implements AnalysisListener {
	
	private int filesRemaining;
	private boolean showUpdates = true;
	private final int read;
    
	public OfflineRunner (String [] filenames, int read) {
        this.read = read;
		
		// See if we need to show updates
		if (System.getProperty("fastqc.quiet") != null && System.getProperty("fastqc.quiet").equals("true")) {
			showUpdates = false;
		}
		
		Vector<File> files = new Vector<File>();
		
		for (int f=0;f<filenames.length;f++) {
			File file = new File(filenames[f]);
			if (!file.exists() || ! file.canRead()) {
				System.err.println("Skipping '"+filenames[f]+"' which didn't exist, or couldn't be read");
				continue;
			}
			files.add(file);
		}
		
		File [][] fileGroups;
		
		// See if we need to group together files from a casava group
		if (System.getProperty("fastqc.casava") != null && System.getProperty("fastqc.casava").equals("true")) {
			fileGroups = CasavaBasename.getCasavaGroups(files.toArray(new File[0]));
		}
		else {
			fileGroups = new File [files.size()][1];
			for (int f=0;f<files.size();f++) {
				fileGroups[f][0] = files.elementAt(f);
			}
		}
		
		
		filesRemaining = fileGroups.length;
		
		for (int i=0;i<fileGroups.length;i++) {

			try {
				processFile(fileGroups[i], read);
			}
			catch (Exception e) {
				System.err.println("Failed to process "+fileGroups[i][0]);
				e.printStackTrace();
				--filesRemaining;
			}
		}
		
		// We need to hold this class open as otherwise the main method
		// exits when it's finished.
		while (filesRemaining > 0) {
			try {
				Thread.sleep(1000);
			} 
			catch (InterruptedException e) {}
		}
		System.exit(0);
		
	}
	
	public void processFile (File [] files, int read) throws Exception {
		for (int f=0;f<files.length;f++) {
			if (!files[f].exists()) {
				throw new IOException(files[f].getName()+" doesn't exist");
			}
		}
		SequenceFile sequenceFile;
		if (files.length == 1) {
			sequenceFile = SequenceFactory.getSequenceFile(files[0], read);
		}
		else {
			sequenceFile = SequenceFactory.getSequenceFile(files, read);
		}
						
		AnalysisRunner runner = new AnalysisRunner(sequenceFile);
		runner.addAnalysisListener(this);
		
		OverRepresentedSeqs os = new OverRepresentedSeqs();
		
		QCModule [] module_list = new QCModule [] {
			new BasicStats(),
			new PerBaseQualityScores(),
			new PerSequenceQualityScores(),
			new PerBaseSequenceContent(),
			new PerBaseGCContent(), 
			new PerSequenceGCContent(),
			new NContent(),
			new SequenceLengthDistribution(),
			os.duplicationLevelModule(),
			os,
			new KmerContent()
		};

		runner.startAnalysis(module_list);

	}	
	
	public void analysisComplete(SequenceFile file, QCModule[] results) {
		File reportFile;
		
		if (showUpdates) System.out.println("Analysis complete for "+file.name());

		
		if (System.getProperty("fastqc.output_dir") != null) {
			String fileName = file.getFile().getName().replaceAll(".gz$","").replaceAll(".bz2$","").replaceAll(".txt$","").replaceAll(".fastq$", "").replaceAll(".sam$", "").replaceAll(".bam$", "")+"_"+read+"_fastqc.zip";
			reportFile = new File(System.getProperty("fastqc.output_dir")+"/"+fileName);						
		}
		else {
			reportFile = new File(file.getFile().getAbsolutePath().replaceAll(".gz$","").replaceAll(".bz2$","").replaceAll(".txt$","").replaceAll(".fastq$", "").replaceAll(".sam$", "").replaceAll(".bam$", "")+"_"+read+"_fastqc.zip");
		}
		
		try {
			new HTMLReportArchive(file, results, reportFile, read);
		}
		catch (Exception e) {
			analysisExceptionReceived(file, e);
			return;
		}
		--filesRemaining;

	}

	public void analysisUpdated(SequenceFile file, int sequencesProcessed, int percentComplete) {
		
		if (percentComplete % 5 == 0) {
			if (percentComplete == 105) {
				if (showUpdates) System.err.println("It seems our guess for the total number of records wasn't very good.  Sorry about that.");
			}
			if (percentComplete > 100) {
				if (showUpdates) System.err.println("Still going at "+percentComplete+"% complete for "+file.name()+" "+read);
			}
			else {
				if (showUpdates) System.err.println("Approx "+percentComplete+"% complete for "+file.name()+" "+read);
			}
		}
	}

	public void analysisExceptionReceived(SequenceFile file, Exception e) {
		System.err.println("Failed to process file "+file.name()+" "+read);
		e.printStackTrace();
		--filesRemaining;
	}

	public void analysisStarted(SequenceFile file) {
		if (showUpdates) System.err.println("Started analysis of "+file.name()+" "+read);
		
	}
	
}
 public class WelcomePanel extends JPanel {

	public WelcomePanel () {
		setLayout(new GridBagLayout());
		GridBagConstraints gbc = new GridBagConstraints();
		
		gbc.gridx=1;
		gbc.gridy=1;
		gbc.weightx=0.5;
		gbc.weighty=0.99;
		
		add(new JPanel(),gbc);
		gbc.gridy++;
		gbc.weighty=0.01;
		
		gbc.insets = new Insets(10, 10, 10, 10);
		gbc.fill = GridBagConstraints.NONE;
		
		add(new FastQCTitlePanel(),gbc);
		
		gbc.gridy++;
		gbc.weighty=0.5;
		
		add(new JLabel("Use File > Open to select the sequence file you want to check"),gbc);
		
		gbc.gridy++;
		gbc.weighty=0.99;
		add(new JPanel(),gbc);
		
	}
}
 public class BAMFileFilter extends FileFilter {

	public boolean accept(File f) {
		return true;
	}

	public String getDescription() {
		return "BAM/SAM Files (all entries)";
	}

}
 public class CasavaFastQFileFilter extends FileFilter {

	public boolean accept(File f) {
		if (f.isDirectory() || f.getName().endsWith(".fastq.gz")) {
			return true;
		}
		return false;
	}

	public String getDescription() {
		return "Casava FastQ Files";
	}

}
 public class FastQFileFilter extends FileFilter {

	public boolean accept(File f) {
		return true;
	}

	public String getDescription() {
		return "FastQ Files";
	}

}
 public class MappedBAMFileFilter extends FileFilter {

	public boolean accept(File f) {
		return true;
	}

	public String getDescription() {
		return "BAM/SAM Files (only mapped entries)";
	}

}
 public class SequenceFileFilter extends FileFilter {

	public boolean accept(File f) {
		if (f.isDirectory() 
				|| f.getName().toLowerCase().endsWith(".txt.gz") 
				|| f.getName().toLowerCase().endsWith(".fastq.gz") 
				|| f.getName().toLowerCase().endsWith(".txt.bz2") 
				|| f.getName().toLowerCase().endsWith(".fastq.bz2")
				|| f.getName().toLowerCase().endsWith(".txt") 
				|| f.getName().toLowerCase().endsWith(".fastq") 
				|| f.getName().toLowerCase().endsWith(".bam") 
				|| f.getName().toLowerCase().endsWith(".sam")
				|| f.getName().toLowerCase().endsWith(".compact-reads")
				|| f.getName().toLowerCase().endsWith(".goby")
				
		) {
			return true;
		}
		else {
			return false;
		}
	}

	public String getDescription() {
		return "Sequence Files";
	}

}
 public class BasicStats implements QCModule {

	private String name = null;
	private int actualCount = 0;
	private int filteredCount = 0;
	private int minLength = 0;
	private int maxLength = 0;
	private long gCount = 0;
	private long cCount = 0;
	private long aCount = 0;
	private long tCount = 0;
	private long nCount = 0;
	private char lowestChar = 126;
	private String fileType = null;
	
	public String description() {
		return "Calculates some basic statistics about the file";
	}
	
	public boolean ignoreFilteredSequences() {
		return false;
	}

	public JPanel getResultsPanel() {
		JPanel returnPanel = new JPanel();
		returnPanel.setLayout(new BorderLayout());
		returnPanel.add(new JLabel("Basic sequence stats",JLabel.CENTER),BorderLayout.NORTH);
		
		TableModel model = new ResultsTable();
		returnPanel.add(new JScrollPane(new JTable(model)),BorderLayout.CENTER);
		
		return returnPanel;
	
	}
	
	public void reset () {
		minLength = 0;
		maxLength = 0;
		gCount = 0;
		cCount = 0;
		aCount = 0;
		tCount = 0;
		nCount = 0;
	}

	public String name() {
		return "Basic Statistics";
	}

	public void processSequence(Sequence sequence) {

		if (name == null) name = sequence.file().name();
		
		// If this is a filtered sequence we simply count it and move on.
		if (sequence.isFiltered()) {
			filteredCount++;
			return;
		}
		
		actualCount++;
		
		if (fileType == null) {
			if (sequence.getColorspace() != null) {
				fileType = "Colorspace converted to bases";
			}
			else {
				fileType = "Conventional base calls";
			}
		}
		
		if (actualCount == 1) {
			minLength = sequence.getSequence().length();
			maxLength = sequence.getSequence().length();
		}
		else {
			if (sequence.getSequence().length() < minLength) minLength = sequence.getSequence().length();
			if (sequence.getSequence().length() > maxLength) maxLength = sequence.getSequence().length();
		}

		char [] chars = sequence.getSequence().toCharArray();
		for (int c=0;c<chars.length;c++) {			
			switch (chars[c]) {
				case 'G': ++gCount;break;
				case 'A': ++aCount;break;
				case 'T': ++tCount;break;
				case 'C': ++cCount;break;
				case 'N': ++nCount;break;			
			}
		}
		
		chars = sequence.getQualityString().toCharArray();
		for (int c=0;c<chars.length;c++) {
			if (chars[c] < lowestChar) {
				lowestChar = chars[c];
			}
		}
	}
	
	public boolean raisesError() {
		return false;
	}

	public boolean raisesWarning() {
		return false;
	}

	public void makeReport(HTMLReportArchive report) {
		ResultsTable table = new ResultsTable();
		
		StringBuffer b = report.htmlDocument();
		StringBuffer d = report.dataDocument();
		
		b.append("<table>\n");
		// Do the headers
		b.append("<tr>\n");
		d.append("#");
		for (int c=0;c<table.getColumnCount();c++) {
			b.append("<th>");
			b.append(table.getColumnName(c));
			d.append(table.getColumnName(c));
			b.append("</th>\n");
			d.append("\t");
		}
		b.append("</tr>\n");
		d.append("\n");
		
		// Do the rows
		for (int r=0;r<table.getRowCount();r++) {
			b.append("<tr>\n");
			for (int c=0;c<table.getColumnCount();c++) {
				b.append("<td>");
				b.append(table.getValueAt(r, c));
				d.append(table.getValueAt(r, c));
				b.append("</td>\n");
				d.append("\t");
			}
			b.append("</tr>\n");
			d.append("\n");
		}
		
		b.append("</table>\n");
				
		
	}

	private class ResultsTable extends AbstractTableModel {
				
		private String [] rowNames = new String [] {
				"Filename",
				"File type",
				"Encoding",
				"Total Sequences",
				"Filtered Sequences",
				"Sequence length",
				"%GC",
		};		
		
		// Sequence - Count - Percentage
		public int getColumnCount() {
			return 2;
		}
	
		public int getRowCount() {
			return rowNames.length;
		}
	
		public Object getValueAt(int rowIndex, int columnIndex) {
			switch (columnIndex) {
				case 0: return rowNames[rowIndex];
				case 1:
					switch (rowIndex) {
					case 0 : return name;
					case 1 : return fileType;
					case 2 : return PhredEncoding.getFastQEncodingOffset(lowestChar);
					case 3 : return ""+actualCount;
					case 4 : return ""+filteredCount;
					case 5 :
						if (minLength == maxLength) {
							return ""+minLength;
						}
						else {
							return minLength+"-"+maxLength;
						}
						
						
					case 6 : 
						if (aCount+tCount+gCount+cCount > 0) {
							return ""+(((gCount+cCount)*100)/(aCount+tCount+gCount+cCount));
						}
						else {
							return 0;
						}
					
					}
			}
			return null;
		}
		
		public String getColumnName (int columnIndex) {
			switch (columnIndex) {
				case 0: return "Measure";
				case 1: return "Value";
			}
			return null;
		}
		
		public Class<?> getColumnClass (int columnIndex) {
			switch (columnIndex) {
			case 0: return String.class;
			case 1: return String.class;
		}
		return null;
			
		}
	}
	

} public class KmerContent implements QCModule {

	private Hashtable<String, Kmer> kmers = new Hashtable<String, Kmer>((int)Math.pow(4, MAX_KMER_SIZE));
	private long gCount = 0;
	private long aCount = 0;
	private long tCount = 0;
	private long cCount = 0;
	
	private int longestSequence = 0;
	private long [][] totalKmerCounts = new long [0][0];
	private long skipCount = 0;
	
	private static final int MIN_KMER_SIZE = 5;
	private static final int MAX_KMER_SIZE = 5;
	
	public boolean calculated = false;
	
	private Kmer [] enrichedKmers = null;
	private double [][] enrichments = null;
	private String [] xCategories = new String[0];
	private String [] xLabels = new String[0];
	
	BaseGroup [] groups;

	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	public JPanel getResultsPanel() {
		
		if (!calculated) calculateEnrichment();
		JPanel returnPanel = new JPanel();
		returnPanel.setLayout(new BorderLayout());
		returnPanel.add(new JLabel("Overrepresented Kmers",JLabel.CENTER),BorderLayout.NORTH);
		
		JSplitPane splitPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
		
		if (enrichedKmers.length > 0) {
			TableModel model = new ResultsTable(enrichedKmers);
			splitPanel.setBottomComponent(new JScrollPane(new JTable(model)));
			splitPanel.setTopComponent(new LineGraph(enrichments, 0d, 100d, "Position in read (bp)", xLabels, xCategories, "Relative enrichment over read length"));
			returnPanel.add(splitPanel,BorderLayout.CENTER);
		}
		else {
			returnPanel.add(new JLabel("There are no overrepresented Kmers",JLabel.CENTER),BorderLayout.CENTER);
		}
		
		return returnPanel;
	}
	
	/**
	 * This method simply keeps a count of the number of Kmers of a given size
	 * seen at each position within the run.  We can use this later on to calculate
	 * the enrichment of the Kmers we actually count.
	 * 
	 * We take in the Kmer sequence even though this isn't used in the total counts
	 * we do this because we don't want to count Kmers with Ns in them, but we do
	 * need to ensure that the data structure is expanded to the right size, and if
	 * we have libraries where later positions are Ns in all sequences then our
	 * data structure ends up too short and we crash. 
	 * 
	 * @param position Position within the read.  0 indexed
	 * @param kmerLength Actual length of the Kmer analysed
	 */
	private void addKmerCount (int position,int kmerLength, String kmer) {
	
		
		if (position >= totalKmerCounts.length) {
			// We need to expand the array
			long [][] newCounts = new long[position+1][];
			for (int i=0;i<totalKmerCounts.length;i++) {
				newCounts[i] = totalKmerCounts[i];
			}
			for (int i=totalKmerCounts.length;i<newCounts.length;i++) {
				newCounts[i] = new long[MAX_KMER_SIZE];
			}
			
			totalKmerCounts = newCounts;
		}
		
		if (kmer.indexOf("N") >=0) return;

		++totalKmerCounts[position][kmerLength-1];
		
	}

	private synchronized void calculateEnrichment () {
		
		// For each kmer we work out the number of times we should have
		// seen this by chance, and then the obs/exp for each of the
		// kmers.  We can then filter for a specific level of enrichment
		// and show only those Kmers
		
		float totalBases = gCount+aCount+tCount+cCount;
		
		float gProb = ((float)gCount)/totalBases;
		float aProb = ((float)aCount)/totalBases;
		float tProb = ((float)tCount)/totalBases;
		float cProb = ((float)cCount)/totalBases;
		
		// We'll be grouping together positions later so make up the groups now
		groups = BaseGroup.makeBaseGroups((longestSequence-MIN_KMER_SIZE)+1);

		Vector<Kmer>enrichedKmers = new Vector<Kmer>();
				
		Iterator<Kmer> rawKmers = kmers.values().iterator();
		
		KMER: while (rawKmers.hasNext()) {
			Kmer k = rawKmers.next();
			
			long totalKmerCount = 0;

			for (int i=0;i<totalKmerCounts.length;i++) {
				totalKmerCount += totalKmerCounts[i][k.sequence().length()-1];
			}
			
			
			float prob = 1;
			char [] chars = k.sequence().toCharArray();
			for (int c=0;c<chars.length;c++) {
				switch (chars[c]) {
				case 'G': prob *= gProb;break;
				case 'A': prob *= aProb;break;
				case 'T': prob *= tProb;break;
				case 'C': prob *= cProb;break;
				default: continue KMER; // Ignore Kmers containing non-GATC chars
				}
			}
			// Now work out how many of these kmers we should have seen
			float predicted = prob * totalKmerCount;
						
			k.setObsExp(k.count()/predicted);
				
			// We shall also calculate the positional variation in obs/exp
				
			float [] obsExpPositions = new float[groups.length];
				
			long [] positionCounts = k.getPositions();
				
			for (int g=0;g<groups.length;g++) {
				// This is a summation of the number of Kmers of this length which
				// fall into this base group
				long totalGroupCount = 0;
				
				// This is a summation of the number of hit Kmers which fall within
				// this base group.
				long totalGroupHits = 0;
				for (int p=groups[g].lowerCount()-1;p<groups[g].upperCount() && p < positionCounts.length ;p++) {
					totalGroupCount += totalKmerCounts[p][chars.length-1];
					totalGroupHits += positionCounts[p];
				}
			
				// We used to have a filter here which provided a default value where there
				// were fewer than 1000 observations at a given position.  This caused breakage
				// in small files where every position had fewer than 1000 observations and since
				// I can't see why this exclusion was there in the first place, I've now removed
				// it.  If the reason for its original inclusion reappears then we can put it back
				// (with a more sensible default value), but for the moment this appears to be the
				// better fix.
				
				predicted = prob * totalGroupCount;
				obsExpPositions[g] = totalGroupHits/predicted;
			}
			k.setObsExpPositions(obsExpPositions);
			
			if (k.obsExp() > 3 || k.maxObsExp() > 5) {
				enrichedKmers.add(k);
			}			
			
		}
		
		Kmer [] finalKMers = enrichedKmers.toArray(new Kmer[0]);
		Arrays.sort(finalKMers);				
		
		// Now we take the enrichment positions for the top 6 hits and
		// record these so we can plot them on a line graph
		enrichments = new double [Math.min(6, finalKMers.length)][];
		xLabels = new String[enrichments.length];
		
		xCategories = new String [groups.length];
		
		for (int i=0;i<xCategories.length;i++) {
			xCategories[i] = groups[i].toString();
		}
		
		for (int k=0;k<enrichments.length;k++) {
			enrichments[k] = new double[groups.length];
			
			float [] obsExpPos = finalKMers[k].getObsExpPositions();
			
			// Find the max enrichment for this sequence
			float max = 0;
			for (int i=0;i<obsExpPos.length;i++) {								
				if (obsExpPos[i] > max) max = obsExpPos[i]; 
			}
			
			for (int g=0;g<groups.length;g++) {				
				enrichments[k][g] = obsExpPos[g]/max * 100; 
			}
			
			xLabels[k] = finalKMers[k].sequence();
			
		}
		
		this.enrichedKmers = finalKMers;		
		
		calculated = true;
	}
	
		
	public void processSequence(Sequence sequence) {
		calculated = false;
		
		++skipCount;
		if (skipCount % 5 != 0) return;
		
		char [] seq = sequence.getSequence().toCharArray();

		if (seq.length > longestSequence) {
			longestSequence = seq.length;
		}
				
		for (int i=0;i<seq.length;i++) {
			switch (seq[i]) {
			case 'G': ++gCount;break;
			case 'A': ++aCount;break;
			case 'T': ++tCount;break;
			case 'C': ++cCount;break;
			}
		}
		
		// Now we go through all of the Kmers to count these
		for (int kmerSize=MIN_KMER_SIZE;kmerSize<=MAX_KMER_SIZE;kmerSize++) {
			for (int i=0;i<=seq.length-kmerSize;i++) {
				
				String kmer = sequence.getSequence().substring(i, i+kmerSize);
				
				// Add to the counts before skipping Kmers containing Ns (see
				// explanation in addKmerCount for the reasoning).
				addKmerCount(i, kmerSize, kmer);
				
				// Skip Kmers containing N
				if (kmer.indexOf("N") >=0) continue;

				if (kmers.containsKey(kmer)) {
					kmers.get(kmer).incrementCount(i);
				}
				else {
					kmers.put(kmer, new Kmer(kmer,i,(seq.length-kmerSize)+1));
				}

			}
		}
	}
	
	public void reset () {
		calculated = false;
		gCount = 0;
		aCount = 0;
		tCount = 0;
		cCount = 0;
		totalKmerCounts = new long[0][0];
		longestSequence = 0;
		skipCount = 0;
		enrichedKmers = null;
		kmers.clear();
	}

	public String description() {
		return "Identifies short sequences which are overrepresented";
	}

	public String name() {
		return "Kmer Content";
	}

	public boolean raisesError() {
		if (!calculated) calculateEnrichment();
		
		// We raise an error if the most enriched kmer is seen more than 100 times
		// more frequently than we expect.
		
		if (enrichedKmers.length > 0 && enrichedKmers[0].maxObsExp() > 10) return true;
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) calculateEnrichment();
		
		// We raise a warning if there are any enriched kmers
		if (enrichedKmers.length > 0) return true;
		return false;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		if (!calculated) calculateEnrichment();
		
		if (enrichedKmers.length > 0) {
			ZipOutputStream zip = report.zipFile();
			zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/kmer_profiles.png"));

			BufferedImage b = new BufferedImage(800,600,BufferedImage.TYPE_INT_RGB);
			Graphics g = b.getGraphics();

			LineGraph lg = new LineGraph(enrichments, 0d, 100d, "Position in read (bp)", xLabels, xCategories, "Relative enrichment over read length");
			lg.paint(g,800,600);

			ImageIO.write((BufferedImage)(b),"PNG",zip);

			StringBuffer sb = report.htmlDocument();

			sb.append("<p><img class=\"indented\" src=\"Images/kmer_profiles.png\" alt=\"Kmer graph\"></p>\n");

		}
		
		
		ResultsTable table = new ResultsTable(enrichedKmers);
		
		StringBuffer b = report.htmlDocument();
		StringBuffer d = report.dataDocument();
		
		if (enrichedKmers.length == 0) {
			b.append("<p>No overrepresented Kmers</p>\n");
		}
		
		else {
			b.append("<table>\n");
			// Do the headers
			b.append("<tr>\n");
			d.append("#");
			for (int c=0;c<table.getColumnCount();c++) {
				b.append("<th>");
				b.append(table.getColumnName(c));
				d.append(table.getColumnName(c));
				b.append("</th>\n");
				if (c<table.getColumnCount()-1) {
					d.append("\t");
				}
			}
			b.append("</tr>\n");
			d.append("\n");
			
			// Do the rows
			for (int r=0;r<table.getRowCount();r++) {
				b.append("<tr>\n");
				for (int c=0;c<table.getColumnCount();c++) {
					b.append("<td>");
					b.append(table.getValueAt(r, c));
					d.append(table.getValueAt(r, c));
					b.append("</td>\n");
					if (c<table.getColumnCount()-1) {
						d.append("\t");
					}
				}
				b.append("</tr>\n");
				d.append("\n");
			}
			
			b.append("</table>\n");
		}	
	}
	
	private class Kmer implements Comparable<Kmer>{
		
		private String sequence;
		private long count = 0;
		private float obsExp = 0;
		private float [] obsExpPositions = null;
		private long [] positions = new long[0];
		
		public Kmer (String sequence, int position, int seqLength) {

			// Do this slightly convoluted dance to try to avoid
			// keeping the whole original sequence in memory
			char [] chars = sequence.toCharArray();
			this.sequence = new String(chars);
			count = 1;
			positions = new long[seqLength];
			++positions[position];
		}
		
		public void incrementCount (int position) {
			++count;
			
			if (position >= positions.length) {
				long [] newPositions = new long[position+1];
				for (int i=0;i<positions.length;i++) {
					newPositions[i] = positions[i];
				}
				positions = newPositions;
			}
			
			++positions[position];
			
		}
		
		public long [] getPositions () {
			return positions;
		}
		
		public String sequence () {
			return sequence;
		}
		
		public long count () {
			return count;
		}
		
		public void setObsExp (float oe) {
			this.obsExp = oe;
		}
		
		public void setObsExpPositions (float [] oePositions) {
			this.obsExpPositions = oePositions;
		}
		
		public float [] getObsExpPositions () {
			return obsExpPositions;
		}
		
		public float obsExp () {
			return obsExp;
		}
		
		public float maxObsExp () {
			float max = 0;
			for (int i=0;i<obsExpPositions.length;i++) {
				if (obsExpPositions[i]>max) max = obsExpPositions[i];
			}
			return max;
		}

		public int maxPosition () {
			float max = 0;
			int position = 0;
			for (int i=0;i<obsExpPositions.length;i++) {
				if (obsExpPositions[i]>max) {
					max = obsExpPositions[i];
					position = i+1;
				}
			}
			
			if (position == 0) {
				System.err.println("No value > 0 for "+sequence);
				position = 1;
			}
			
			return position;
		}

		public int compareTo(Kmer o) {
			return Float.compare(o.obsExp(), obsExp());
		}
	}
	
	private class ResultsTable extends AbstractTableModel {
		
		private Kmer [] kmers;
		
		public ResultsTable (Kmer [] kmers) {
			this.kmers = kmers;
		}
		
		
		// Sequence - Count - Obs/Exp
		public int getColumnCount() {
			return 5;
		}

		public int getRowCount() {
			return kmers.length;
		}

		public Object getValueAt(int rowIndex, int columnIndex) {
			switch (columnIndex) {
				case 0: return kmers[rowIndex].sequence();
				case 1: return kmers[rowIndex].count()*5;
				case 2: return kmers[rowIndex].obsExp();
				case 3: return kmers[rowIndex].maxObsExp();
				case 4: return groups[kmers[rowIndex].maxPosition()-1].toString();
						
			}
			return null;
		}
		
		public String getColumnName (int columnIndex) {
			switch (columnIndex) {
				case 0: return "Sequence";
				case 1: return "Count";
				case 2: return "Obs/Exp Overall";
				case 3: return "Obs/Exp Max";
				case 4: return "Max Obs/Exp Position";
			}
			return null;
		}
		
		public Class<?> getColumnClass (int columnIndex) {
			switch (columnIndex) {
			case 0: return String.class;
			case 1: return Integer.class;
			case 2: return Float.class;
			case 3: return Float.class;
			case 4: return String.class;
		}
		return null;
			
		}
	}

}
 public class OverRepresentedSeqs implements QCModule {

	protected HashMap<String, Integer>sequences = new HashMap<String, Integer>();
	protected int count = 0;
	private OverrepresentedSeq [] overrepresntedSeqs = null;
	private boolean calculated = false;
	private boolean frozen = false;
	private DuplicationLevel duplicationModule;
	
	// This is the number of different sequences we want to track
	private final int OBSERVATION_CUTOFF = 200000;
	// This is a count of how many unique sequences we've seen so far
	// so we know when to stop adding them.
	private int uniqueSequenceCount = 0;
	// This was the total count at the point at which we saw our total
	// number of unique sequences, so we know what to correct by when
	// extrapolating to the whole file
	protected int countAtUniqueLimit = 0;
	
	
	public OverRepresentedSeqs () {
		duplicationModule = new DuplicationLevel(this);
	}
	
	public String description() {
		return "Identifies sequences which are overrepresented in the set";
	}
	
	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	public DuplicationLevel duplicationLevelModule () {
		return duplicationModule;
	}

	public JPanel getResultsPanel() {
		JPanel returnPanel = new JPanel();
		returnPanel.setLayout(new BorderLayout());
		returnPanel.add(new JLabel("Overrepresented sequences",JLabel.CENTER),BorderLayout.NORTH);
		
		if (!calculated) getOverrepresentedSeqs();
		
		if (overrepresntedSeqs.length > 0) {
			TableModel model = new ResultsTable(overrepresntedSeqs);
			JTable table = new JTable(model);
			table.setCellSelectionEnabled(true);
			returnPanel.add(new JScrollPane(table),BorderLayout.CENTER);
		}
		else {
			returnPanel.add(new JLabel("There are no overrepresented sequences",JLabel.CENTER),BorderLayout.CENTER);
		}
		
		return returnPanel;
	
	}
	
	public DuplicationLevel getDuplicationLevelModule () {
		return duplicationModule;
	}
	private synchronized void getOverrepresentedSeqs () {

		// If the duplication module hasn't already done
		// its calculation it needs to do it now before
		// we stomp all over the data
		duplicationModule.calculateLevels();
		
		Iterator<String> s = sequences.keySet().iterator();
		List<OverrepresentedSeq>keepers = new ArrayList<OverrepresentedSeq>();
		
		while (s.hasNext()) {
			String seq = s.next();
			double percentage = ((double)sequences.get(seq)/count)*100;
			if (percentage > 0.1) {
				OverrepresentedSeq os = new OverrepresentedSeq(seq, sequences.get(seq), percentage);
				keepers.add(os);
			}
		}
		
		overrepresntedSeqs = keepers.toArray(new OverrepresentedSeq[0]);
		Arrays.sort(overrepresntedSeqs);
		calculated  = true;
		sequences.clear();
		
	}
	
	public void reset () {
		count = 0;
		sequences.clear();
	}

	public String name() {
		return "Overrepresented sequences";
	}

	public void processSequence(Sequence sequence) {
		
		calculated = false;
		
		++count;
		
		// Since we rely on identity to match sequences we can't trust really long
		// sequences, so anything over 75bp gets truncated to 50bp.
		String seq = sequence.getSequence();
		if (seq.length() > 75) {
			seq = new String(seq.substring(0, 50));
		}
				
		if (sequences.containsKey(seq)) {
			sequences.put(seq, sequences.get(seq)+1);
		}
		else {
			if (! frozen) {
				sequences.put(seq, 1);
				++uniqueSequenceCount;
				countAtUniqueLimit = count;
				if (uniqueSequenceCount == OBSERVATION_CUTOFF) {
					frozen = true;
				}

			}
		}		
	}
	
	private class ResultsTable extends AbstractTableModel {
		
		private OverrepresentedSeq [] seqs;
		
		public ResultsTable (OverrepresentedSeq [] seqs) {
			this.seqs = seqs;
		}
		
		
		// Sequence - Count - Percentage
		public int getColumnCount() {
			return 4;
		}

		public int getRowCount() {
			return seqs.length;
		}

		public Object getValueAt(int rowIndex, int columnIndex) {
			switch (columnIndex) {
				case 0: return seqs[rowIndex].seq();
				case 1: return seqs[rowIndex].count();
				case 2: return seqs[rowIndex].percentage();
				case 3: return seqs[rowIndex].contaminantHit();
					
			}
			return null;
		}
		
		public String getColumnName (int columnIndex) {
			switch (columnIndex) {
				case 0: return "Sequence";
				case 1: return "Count";
				case 2: return "Percentage";
				case 3: return "Possible Source";
			}
			return null;
		}
		
		public Class<?> getColumnClass (int columnIndex) {
			switch (columnIndex) {
			case 0: return String.class;
			case 1: return Integer.class;
			case 2: return Double.class;
			case 3: return String.class;
		}
		return null;
			
		}
	}
	
	private class OverrepresentedSeq implements Comparable<OverrepresentedSeq>{
		
		private String seq;
		private int count;
		private double percentage;
		private ContaminantHit contaminantHit;
		
		public OverrepresentedSeq (String seq, int count, double percentage) {
			this.seq = seq;
			this.count = count;
			this.percentage = percentage;
			this.contaminantHit = ContaminentFinder.findContaminantHit(seq);
		}
		
		public String seq () {
			return seq;
		}
		
		public int count () {
			return count;
		}
		
		public double percentage () {
			return percentage;
		}
		
		public String contaminantHit () {
			if (contaminantHit == null) {
				return "No Hit";
			}
			else {
				return contaminantHit.toString();
			}
		}

		public int compareTo(OverrepresentedSeq o) {
			return o.count-count;
		}
	}

	public boolean raisesError() {
		if (!calculated) getOverrepresentedSeqs();
		if (overrepresntedSeqs.length>0) {
			if (overrepresntedSeqs[0].percentage > 1) {
				return true;
			}
		}
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) getOverrepresentedSeqs();

		if (overrepresntedSeqs.length > 0) return true;
		return false;
	}

	public void makeReport(HTMLReportArchive report) {
		if (!calculated) getOverrepresentedSeqs();
		ResultsTable table = new ResultsTable(overrepresntedSeqs);
		
		StringBuffer b = report.htmlDocument();
		StringBuffer d = report.dataDocument();
		
		if (overrepresntedSeqs.length == 0) {
			b.append("<p>No overrepresented sequences</p>\n");
		}
		
		else {
			b.append("<table>\n");
			// Do the headers
			b.append("<tr>\n");
			d.append("#");
			for (int c=0;c<table.getColumnCount();c++) {
				b.append("<th>");
				b.append(table.getColumnName(c));
				d.append(table.getColumnName(c));
				b.append("</th>\n");
				if (c<table.getColumnCount()-1) {
					d.append("\t");
				}
			}
			b.append("</tr>\n");
			d.append("\n");
			
			// Do the rows
			for (int r=0;r<table.getRowCount();r++) {
				b.append("<tr>\n");
				for (int c=0;c<table.getColumnCount();c++) {
					b.append("<td>");
					b.append(table.getValueAt(r, c));
					d.append(table.getValueAt(r, c));
					b.append("</td>\n");
					if (c<table.getColumnCount()-1) {
						d.append("\t");
					}
				}
				b.append("</tr>\n");
				d.append("\n");
			}
			
			b.append("</table>\n");
		}	
	}

} public class PerBaseGCContent implements QCModule {

	public long [] gcCounts = new long [0];
	public long [] atCounts = new long [0];
	private double [] percentages = null;
	private String [] xCategories = new String[0];
	private boolean calculated = false;
	private double maxDeviation;
	
	
	public JPanel getResultsPanel() {
		
		if (!calculated) getPercentages();

		return new LineGraph(new double [][] {percentages}, 0d, 100d, "Position in read (bp)", new String [] {"%GC"}, xCategories, "GC content across all bases");
	}

	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	private synchronized void getPercentages () {

		BaseGroup [] groups = BaseGroup.makeBaseGroups(gcCounts.length);
		
		xCategories = new String[groups.length];

		percentages = new double [groups.length];

		
		// For error calling we also want to work out what the
		// maximum deviation from the mean GC is to see if we
		// have any great level of variation
		double mean = 0;

		long total;
		long gcCount;

		for (int i=0;i<groups.length;i++) {
						
			xCategories[i] = groups[i].toString();

			gcCount = 0;
			total = 0;
			
			for (int bp=groups[i].lowerCount()-1;bp<groups[i].upperCount();bp++) {		
				gcCount += gcCounts[bp];
				total += gcCounts[bp];
				total += atCounts[bp];
			}
			
			percentages[i] = 100*(gcCount/(double)total);
			mean += percentages[i];
		}
		
		mean /= percentages.length;
		
		// We now calculate the maximumn percentage deviation
		maxDeviation = Math.abs(mean - percentages[0]);
		for (int i=1;i<percentages.length;i++) {
			double diff = Math.abs(mean-percentages[i]);
			if (diff > maxDeviation) {
				maxDeviation = diff;
			}
		}
		
		calculated = true;
	}
	
	public void processSequence(Sequence sequence) {
		calculated = false;
		char [] seq = sequence.getSequence().toCharArray();

		if (gcCounts.length < seq.length) {
			// We need to expand the size of the data structures
			
			long [] gcCountsNew = new long [seq.length];
			long [] atCountsNew = new long [seq.length];

			for (int i=0;i<gcCounts.length;i++) {
				gcCountsNew[i] = gcCounts[i];
				atCountsNew[i] = atCounts[i];
			}
			
			gcCounts = gcCountsNew;
			atCounts = atCountsNew;
		}
		
		for (int i=0;i<seq.length;i++) {
			if (seq[i] == 'G' || seq[i] == 'C') {
				++gcCounts[i];
			}
			if (seq[i] == 'A' || seq[i] == 'T' || seq[i] == 'U') {
				++atCounts[i];
			}
		}
		
	}
	
	public void reset () {
		gcCounts = new long[0];
		atCounts = new long[0];
	}

	public String description() {
		return "Shows the GC content of all bases at a given position in a sequencing run";
	}

	public String name() {
		return "Per base GC content";
	}

	public boolean raisesError() {
		if (!calculated) getPercentages();
		
		return maxDeviation > 10;
	}

	public boolean raisesWarning() {
		if (!calculated) getPercentages();

		return maxDeviation > 5;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		if (!calculated) getPercentages();
		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_base_gc_content.png"));

		BufferedImage b = new BufferedImage(Math.max(800, percentages.length*15),600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();

		LineGraph lg = new LineGraph(new double [][] {percentages}, 0d, 100d, "Position in read (bp)", new String [] {"%GC"}, xCategories, "GC content across all bases");
		lg.paint(g,b.getWidth(),b.getHeight());

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();

		sb.append("<p><img class=\"indented\" src=\"Images/per_base_gc_content.png\" alt=\"Per base GC content graph\"></p>\n");	
		
		sb = report.dataDocument();
		sb.append("#Base\t%GC\n");
		for (int i=0;i<xCategories.length;i++) {
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(percentages[i]);
			sb.append("\n");
		}
	} 

}
 public class NContent implements QCModule {

	public long [] nCounts = new long [0];
	public long [] notNCounts = new long [0];
	public boolean calculated = false;
	public double [] percentages = null;
	public String [] xCategories = new String[0];
	
	public JPanel getResultsPanel() {
		
		if (!calculated) getPercentages();
		return new LineGraph(new double [][] {percentages}, 0d, 100d, "Position in read (bp)",new String [] {"%N"}, xCategories, "N content across all bases");
	}

	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	private synchronized void getPercentages () {
		
		BaseGroup [] groups = BaseGroup.makeBaseGroups(nCounts.length);
		
		xCategories = new String[groups.length];

		percentages = new double [groups.length];

		long total;
		long nCount;

		for (int i=0;i<groups.length;i++) {
						
			xCategories[i] = groups[i].toString();

			nCount = 0;
			total = 0;
			
			for (int bp=groups[i].lowerCount()-1;bp<groups[i].upperCount();bp++) {		
				nCount += nCounts[bp];
				total += nCounts[bp];
				total += notNCounts[bp];
			}
			
			percentages[i] = 100*(nCount/(double)total);
		}
				
		calculated = true;
		
	}
		
	public void processSequence(Sequence sequence) {
		calculated = false;
		char [] seq = sequence.getSequence().toCharArray();
		if (nCounts.length < seq.length) {
			// We need to expand the size of the data structures
			
			long [] nCountsNew = new long [seq.length];
			long [] notNCountsNew = new long [seq.length];

			for (int i=0;i<nCounts.length;i++) {
				nCountsNew[i] = nCounts[i];
				notNCountsNew[i] = notNCounts[i];
			}
			
			nCounts = nCountsNew;
			notNCounts = notNCountsNew;
		}
		
		for (int i=0;i<seq.length;i++) {
			if (seq[i] == 'N') {
				++nCounts[i];
			}
			else {
				++notNCounts[i];
			}
		}
		
	}
	
	public void reset () {
		nCounts = new long[0];
		notNCounts = new long[0];
	}

	public String description() {
		return "Shows the percentage of bases at each position which are not being called";
	}

	public String name() {
		return "Per base N content";
	}

	public boolean raisesError() {
		if (!calculated) getPercentages();
		for (int i=0;i<percentages.length;i++) {
			if (percentages[i] > 20) {
				return true;
			}
		}
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) getPercentages();
		for (int i=0;i<percentages.length;i++) {
			if (percentages[i] > 5) {
				return true;
			}
		}
		return false;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		if (!calculated) getPercentages();
		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_base_n_content.png"));
		
		BufferedImage b = new BufferedImage(Math.max(800, percentages.length*15),600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();
		
		LineGraph lg = new LineGraph(new double [][] {percentages}, 0d, 100d, "Position in read (bp)", new String [] {"%N"}, xCategories, "N content across all bases");
		lg.paint(g,b.getWidth(),b.getHeight());

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();
		
		sb.append("<p><img class=\"indented\" src=\"Images/per_base_n_content.png\" alt=\"N content graph\"></p>\n");
		
		sb = report.dataDocument();
		sb.append("#Base\tN-Count\n");
		for (int i=0;i<xCategories.length;i++) {
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(percentages[i]);
			sb.append("\n");
		}
	}

}
 public class PerBaseQualityScores implements QCModule {

	public QualityCount [] qualityCounts = new QualityCount[0];
	double [] means = null;
	double [] medians = null;
	double [] lowerQuartile = null;
	double [] upperQuartile = null;
	double [] lowest = null;
	double [] highest = null;
	String [] xLabels;
	int low = 0;
	int high = 0;
	PhredEncoding encodingScheme;
	private boolean calculated = false;
	
	
	public JPanel getResultsPanel() {
		
		if (!calculated) getPercentages();

		return new QualityBoxPlot(means,medians,lowest,highest,lowerQuartile,upperQuartile, low, high, 2d, xLabels, "Quality scores across all bases ("+encodingScheme+" encoding)");
	}
	
	public boolean ignoreFilteredSequences() {
		return true;
	}

	private synchronized void getPercentages () {
		
		char [] range = calculateOffsets();
		encodingScheme = PhredEncoding.getFastQEncodingOffset(range[0]);
		low = 0;
		high = range[1] - encodingScheme.offset();
		if (high < 35) {
			high = 35;
		}
		
		BaseGroup [] groups = BaseGroup.makeBaseGroups(qualityCounts.length);
		
		means = new double[groups.length];
		medians = new double[groups.length];
		lowest = new double[groups.length];
		highest = new double[groups.length];
		lowerQuartile = new double[groups.length];
		upperQuartile = new double[groups.length];
		xLabels = new String[groups.length];
		
		for (int i=0;i<groups.length;i++) {
			xLabels[i] = groups[i].toString();
			int minBase = groups[i].lowerCount();
			int maxBase = groups[i].upperCount();
			lowest[i] = getPercentile(minBase, maxBase, encodingScheme.offset(), 10);
			highest[i] = getPercentile(minBase, maxBase, encodingScheme.offset(), 90);
			means[i] = getMean(minBase,maxBase,encodingScheme.offset());
			medians[i] = getPercentile(minBase, maxBase, encodingScheme.offset(), 50);
			lowerQuartile[i] = getPercentile(minBase, maxBase, encodingScheme.offset(), 25);
			upperQuartile[i] = getPercentile(minBase, maxBase, encodingScheme.offset(), 75);
		}

		calculated = true;

	}
	
	private char [] calculateOffsets () {
		// Works out from the set of chars what is the most
		// likely encoding scale for this file.
		
		char minChar = 0;
		char maxChar = 0;
		
		for (int q=0;q<qualityCounts.length;q++) {
			if (q == 0) {
				minChar = qualityCounts[q].getMinChar();
				maxChar = qualityCounts[q].getMaxChar();
			}
			else {
				if (qualityCounts[q].getMinChar() < minChar) {
					minChar = qualityCounts[q].getMinChar();
				}
				if (qualityCounts[q].getMaxChar() > maxChar) {
					maxChar = qualityCounts[q].getMaxChar();
				}
			}
		}
		
		return new char[] {minChar,maxChar};
	}
	
	public void processSequence(Sequence sequence) {
		calculated = false;
		char [] qual = sequence.getQualityString().toCharArray();
		if (qualityCounts.length < qual.length) {
			
			QualityCount [] qualityCountsNew = new QualityCount[qual.length];
			
			for (int i=0;i<qualityCounts.length;i++) {
				qualityCountsNew[i] = qualityCounts[i];
			}
			for (int i=qualityCounts.length;i<qualityCountsNew.length;i++) {
				qualityCountsNew[i] = new QualityCount();				
			}
			qualityCounts = qualityCountsNew;
			
		}
		
		for (int i=0;i<qual.length;i++) {
			qualityCounts[i].addValue(qual[i]);
		}
		
	}
	
	public void reset () {
		qualityCounts = new QualityCount[0];
	}

	public String description() {
		return "Shows the Quality scores of all bases at a given position in a sequencing run";
	}

	public String name() {
		return "Per base sequence quality";
	}

	public boolean raisesError() {
		if (!calculated) getPercentages();

		for (int i=0;i<lowerQuartile.length;i++) {
			if (lowerQuartile[i] < 5 || medians[i] < 20) {
				return true;
			}
		}
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) getPercentages();

		for (int i=0;i<lowerQuartile.length;i++) {
			if (lowerQuartile[i] < 10 || medians[i] < 25) {
				return true;
			}
		}
		return false;
	}
	
	public void makeReport(HTMLReportArchive report) throws IOException {
		
		if (!calculated) getPercentages();

		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_base_quality.png"));
		
		BufferedImage b = new BufferedImage(Math.max(800, means.length*15),600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();
		QualityBoxPlot bp = new QualityBoxPlot(means,medians,lowest,highest,lowerQuartile,upperQuartile, 0, high, 2d, xLabels, "Quality scores across all bases ("+encodingScheme+" encoding)");
		bp.paint(g,b.getWidth(),b.getHeight());

	
		ImageIO.write((BufferedImage)(b),"PNG",zip);
	
		StringBuffer sb = report.htmlDocument();
		
		sb.append("<p><img class=\"indented\" src=\"Images/per_base_quality.png\" alt=\"Per base quality graph\"></p>\n");
		
		sb = report.dataDocument();
		sb.append("#Base\tMean\tMedian\tLower Quartile\tUpper Quartile\t10th Percentile\t90th Percentile\n");
		for (int i=0;i<means.length;i++) {
			sb.append(xLabels[i]);
			sb.append("\t");

			sb.append(means[i]);
			sb.append("\t");

			sb.append(medians[i]);
			sb.append("\t");

			sb.append(lowerQuartile[i]);
			sb.append("\t");

			sb.append(upperQuartile[i]);
			sb.append("\t");

			sb.append(lowest[i]);
			sb.append("\t");

			sb.append(highest[i]);

			sb.append("\n");
		}
	}
	
	private double getPercentile (int minbp, int maxbp, int offset, int percentile) {
		int count = 0;
		double total = 0;
	
		for (int i=minbp-1;i<maxbp;i++) {
			if (qualityCounts[i].getTotalCount() > 100) {
				count++;
				total += qualityCounts[i].getPercentile(offset, percentile);
			}
		}
		
		if (count > 0) {
			return total/count;
		}
		return 0;
		
	}

	private double getMean (int minbp, int maxbp, int offset) {
		int count = 0;
		double total = 0;
	
		for (int i=minbp-1;i<maxbp;i++) {
			if (qualityCounts[i].getTotalCount() > 0) {
				count++;
				total += qualityCounts[i].getMean(offset);
			}
		}
		
		if (count > 0) {
			return total/count;
		}
		return 0;
		
	}

	
	private class QualityCount {
		private HashMap<Character, Long> counts = new HashMap<Character, Long>();
		
		private long totalCounts = 0;
	
		public void addValue(char c) {
			totalCounts++;
			if (counts.containsKey(c)) {
				counts.put(c, counts.get(c)+1);
			}
			else {
				counts.put(c, 1L);
			}
		}
		
		public long getTotalCount () {
			return totalCounts;
		}
		
		public char getMinChar () {
			char minChar = 10000;
			Iterator<Character>c = counts.keySet().iterator();
			while (c.hasNext()) {
				Character thisChar = c.next();
				if (thisChar < minChar) minChar = thisChar;
			}
			
			return minChar;
		}
		
		public char getMaxChar () {
			char maxChar = 0;

			Iterator<Character>c = counts.keySet().iterator();
			while (c.hasNext()) {
				Character thisChar = c.next();
				if (thisChar > maxChar) maxChar = thisChar;
			}
			
			return maxChar;

		}
				
		public double getMean (int offset) {
			long total = 0;
			long count = 0;
			Iterator<Character>c = counts.keySet().iterator();
			while (c.hasNext()) {
				Character thisChar = c.next();
				total += counts.get(thisChar) * (thisChar-offset);
				count += counts.get(thisChar);
			}
			
			return ((double)total)/count;
		}
		
		public double getPercentile (int offset, int percentile) {
			Character [] chars = counts.keySet().toArray(new Character[0]);
			Arrays.sort(chars);
			long total = 0;
			for (int c=0;c<chars.length;c++) {
				total += counts.get(chars[c]);
			}
			
			total *= percentile;
			total /= 100;
			
			long count = 0;
			for (int c=0;c<chars.length;c++) {
				count += counts.get(chars[c]);
				if (count >= total) {
					return (chars[c]-offset);
				}
			}
			
			return -1;
			
		}
		
	}

}
 public class PerBaseSequenceContent implements QCModule {

	public long [] gCounts = new long [0];
	public long [] aCounts = new long [0];
	public long [] cCounts = new long [0];
	public long [] tCounts = new long [0];
	private double [][] percentages = null;
	private String [] xCategories = new String[0];
	private boolean calculated = false;
	
	
	public JPanel getResultsPanel() {
		
		if (!calculated) getPercentages();

		return new LineGraph(percentages, 0d, 100d, "Position in read (bp)", new String [] {"%T","%C","%A","%G"}, xCategories, "Sequence content across all bases");
	}
	
	public boolean ignoreFilteredSequences() {
		return true;
	}

	private synchronized void getPercentages () {

		BaseGroup [] groups = BaseGroup.makeBaseGroups(gCounts.length);
		
		xCategories = new String[groups.length];

		
		double [] gPercent = new double[groups.length];
		double [] aPercent = new double[groups.length];
		double [] tPercent = new double[groups.length];
		double [] cPercent = new double[groups.length];

		long total;
		long gCount;
		long aCount;
		long tCount;
		long cCount;

		for (int i=0;i<groups.length;i++) {
						
			xCategories[i] = groups[i].toString();

			gCount = 0;
			aCount = 0;
			tCount = 0;
			cCount = 0;
			total = 0;
			
			for (int bp=groups[i].lowerCount()-1;bp<groups[i].upperCount();bp++) {

				total += gCounts[bp];
				total += cCounts[bp];
				total += aCounts[bp];
				total += tCounts[bp];

				aCount += aCounts[bp];
				tCount += tCounts[bp];
				cCount += cCounts[bp];
				gCount += gCounts[bp];				
			}
			
			gPercent[i] = (gCount/(double)total)*100;
			aPercent[i] = (aCount/(double)total)*100;
			tPercent[i] = (tCount/(double)total)*100;
			cPercent[i] = (cCount/(double)total)*100;			
						
		}
		
		percentages = new double [][] {tPercent,cPercent,aPercent,gPercent};
		
		calculated = true;
	}
	
	public void processSequence(Sequence sequence) {
		calculated = false;
		char [] seq = sequence.getSequence().toCharArray();
		if (gCounts.length < seq.length) {
			
			long [] gCountsNew = new long [seq.length];
			long [] aCountsNew = new long [seq.length];
			long [] cCountsNew = new long [seq.length];
			long [] tCountsNew = new long [seq.length];

			for (int i=0;i<gCounts.length;i++) {
				gCountsNew[i] = gCounts[i];
				aCountsNew[i] = aCounts[i];
				tCountsNew[i] = tCounts[i];
				cCountsNew[i] = cCounts[i];
			}		

			gCounts = gCountsNew;
			aCounts = aCountsNew;
			tCounts = tCountsNew;
			cCounts = cCountsNew;
		}
		
		for (int i=0;i<seq.length;i++) {
			if (seq[i] == 'G') {
				++gCounts[i];
			}
			else if (seq[i] == 'A') {
				++aCounts[i];
			}
			else if (seq[i] == 'T') {
				++tCounts[i];
			}
			else if (seq[i] == 'C') {
				++cCounts[i];
			}
		}
		
	}
	
	public void reset () {
		gCounts = new long[0];
		aCounts = new long[0];
		tCounts = new long[0];
		cCounts = new long[0];
	}

	public String description() {
		return "Shows the relative amounts of each base at each position in a sequencing run";
	}

	public String name() {
		return "Per base sequence content";
	}

	public boolean raisesError() {
		if (!calculated) getPercentages();

		// Percentages come in the order GATC
		for (int i=0;i<percentages[0].length;i++) {

			double gcDiff = Math.abs(percentages[0][i]-percentages[3][i]);
			double atDiff = Math.abs(percentages[1][i]-percentages[2][i]);
			
			if (gcDiff > 20 || atDiff > 20) return true;
			
		}
		return false;
	}

	public boolean raisesWarning() {

		if (!calculated) getPercentages();

		// Percentages come in the order GATC
		for (int i=0;i<percentages[0].length;i++) {

			double gcDiff = Math.abs(percentages[0][i]-percentages[3][i]);
			double atDiff = Math.abs(percentages[1][i]-percentages[2][i]);
			
			if (gcDiff > 10 || atDiff > 10) return true;
			
		}
		return false;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		
		if (!calculated) getPercentages();
		
		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_base_sequence_content.png"));

		BufferedImage b = new BufferedImage(Math.max(800, xCategories.length*15),600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();

		LineGraph lg = new LineGraph(percentages, 0d, 100d,  "Position in read (bp)",new String [] {"%T","%C","%A","%G"}, xCategories, "Sequence content across all bases");
		lg.paint(g,b.getWidth(),b.getHeight());

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();

		sb.append("<p><img class=\"indented\" src=\"Images/per_base_sequence_content.png\" alt=\"Per base sequence content\"></p>\n");
		
		sb = report.dataDocument();
		sb.append("#Base\tG\tA\tT\tC\n");
		for (int i=0;i<xCategories.length;i++) {
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(percentages[3][i]);
			sb.append("\t");
			sb.append(percentages[2][i]);
			sb.append("\t");
			sb.append(percentages[0][i]);
			sb.append("\t");
			sb.append(percentages[1][i]);
			sb.append("\n");
		}
		
	} 

}
 public class PerSequenceGCContent implements QCModule {

	private double [] gcDistribution = new double[101];
	private double [] theoreticalDistribution  = new double[101];
	private int [] xCategories = new int[0];
	private double max = 0;
	private double deviationPercent;
	private boolean calculated = false;
	
	private Hashtable<Integer, GCModel> cachedModels = new Hashtable<Integer, GCModel>();
	
	public JPanel getResultsPanel() {
	
		if (!calculated) calculateDistribution();
				
		return new LineGraph(new double [][] {gcDistribution,theoreticalDistribution}, 0d, max, "Mean GC content (%)", new String [] {"GC count per read","Theoretical Distribution"}, xCategories, "GC distribution over all sequences");
	}
	
	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	
	private synchronized void calculateDistribution () {
		max = 0;
		xCategories = new int[gcDistribution.length];
		double totalCount = 0;
		
		
		// We use the mode to calculate the theoretical distribution
		// so that we cope better with skewed distributions.
		int firstMode = 0;
		double modeCount = 0;
		
		for (int i=0;i<gcDistribution.length;i++) {
			xCategories[i] = i;
			totalCount += gcDistribution[i];
			
			if (gcDistribution[i] > modeCount) {
				modeCount = gcDistribution[i];
				firstMode = i;
			}
			if (gcDistribution[i] > max) max = gcDistribution[i];
		}
				
		// The mode might not be a very good measure of the centre
		// of the distribution either due to duplicated vales or
		// several very similar values next to each other.  We therefore
		// average over adjacent points which stay above 95% of the modal
		// value

		double mode = 0;
		int modeDuplicates = 0;
		
		boolean fellOffTop = true;

		for (int i=firstMode;i<gcDistribution.length;i++) {
			if (gcDistribution[i] > gcDistribution[firstMode] - (gcDistribution[firstMode]/10)) {
				mode += i;
				modeDuplicates++;
			}
			else {
				fellOffTop = false;
				break;
			}
		}

		boolean fellOffBottom = true;
		
		for (int i=firstMode-1;i>=0;i--) {
			if (gcDistribution[i] > gcDistribution[firstMode] - (gcDistribution[firstMode]/10)) {
				mode += i;
				modeDuplicates++;
			}
			else {
				fellOffBottom = false;
				break;
			}
		}

		if (fellOffBottom || fellOffTop) {
			// If the distribution is so skewed that 95% of the mode
			// is off the 0-100% scale then we keep the mode as the 
			// centre of the model
			mode = firstMode;
		}
		else {
			mode /= modeDuplicates;
		}
		
		
		
		// We can now work out a theoretical distribution
		double stdev = 0;
		
		for (int i=0;i<gcDistribution.length;i++) {
			stdev += Math.pow((i-mode),2) * gcDistribution[i];
		}
		
		stdev /= totalCount-1;
		
		stdev = Math.sqrt(stdev);
		
		NormalDistribution nd = new NormalDistribution(mode, stdev);
		
		deviationPercent = 0;
		
		for (int i=0;i<theoreticalDistribution.length;i++) {
			double probability = nd.getZScoreForValue(i);
			theoreticalDistribution[i] = probability*totalCount;
			
			if (theoreticalDistribution[i] > max) {
				max = theoreticalDistribution[i];
			}
			
			deviationPercent += Math.abs(theoreticalDistribution[i]-gcDistribution[i]);
		}
		
		deviationPercent /= totalCount;
		deviationPercent *= 100;
		
//		System.out.println("Percentage deviation from normality is "+deviationPercent);
		
		
		calculated = true;
	}

	public void processSequence(Sequence sequence) {
		
		// Because we keep a model around for every possible sequence length we
		// encounter we need to reduce the number of models.  We can do this by
		// rounding off the sequence once we get above a certain size
		
		
		char [] seq = truncateSequence(sequence);
		
		if (seq.length == 0) return; // Ignore empty sequences
		
		
		int thisSeqGCCount = 0;
		for (int i=0;i<seq.length;i++) {
			if (seq[i] == 'G' || seq[i] == 'C') {
				++thisSeqGCCount;
			}
		}

		if (! cachedModels.containsKey(seq.length)) {
			cachedModels.put(seq.length,new GCModel(seq.length));
		}

		GCModelValue [] values = cachedModels.get(seq.length).getModelValues(thisSeqGCCount);

		for (int i=0;i<values.length;i++) {
			gcDistribution[values[i].percentage()] += values[i].increment();
		}
		
	}
	
	private char [] truncateSequence (Sequence sequence) {
		
		String seq = sequence.getSequence();
		
		// TODO: We should return a random chunk of sequence, rather
		// than the start.
		
		if (seq.length() > 1000) {
			int length = (seq.length()/1000)*1000;
			return seq.substring(0, length).toCharArray();
		}
		if (seq.length() > 100) {
			int length = (seq.length()/100)*100;
			return seq.substring(0, length).toCharArray();
		}

		return seq.toCharArray();		
		
	}
	
	public void reset () {
		gcDistribution = new double[101];
	}

	public String description() {
		return "Shows the distribution of GC contents for whole sequences";
	}

	public String name() {
		return "Per sequence GC content";
	}

	public boolean raisesError() {
		if (!calculated) calculateDistribution();

		return deviationPercent > 30;
	}

	public boolean raisesWarning() {
		if (!calculated) calculateDistribution();

		return deviationPercent > 15;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_sequence_gc_content.png"));

		BufferedImage b = new BufferedImage(800,600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();

		LineGraph lg = new LineGraph(new double [][] {gcDistribution,theoreticalDistribution}, 0d, max, "Mean GC content (%)", new String [] {"GC count per read","Theoretical Distribution"}, xCategories, "GC distribution over all sequences");

		lg.paint(g,800,600);

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();

		sb.append("<p><img class=\"indented\" src=\"Images/per_sequence_gc_content.png\" alt=\"Per sequence GC content graph\"></p>\n");

		sb = report.dataDocument();
		sb.append("#GC Content\tCount\n");
		for (int i=0;i<xCategories.length;i++) {
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(gcDistribution[i]);
			sb.append("\n");
		}
	}
}
 public class PerSequenceQualityScores implements QCModule {

	private HashMap<Integer, Long> averageScoreCounts = new HashMap<Integer, Long>();
	private double [] qualityDistribution = null;
	private int [] xCategories = new int[0];
	private char lowestChar = 126;
	private int maxCount = 0;
	private int mostFrequentScore;
	private boolean calculated = false;
	
	public JPanel getResultsPanel() {
	
		if (!calculated) calculateDistribution();
				
		return new LineGraph(new double [][] {qualityDistribution}, 0d, maxCount, "Mean Sequence Quality (Phred Score)",new String [] {"Average Quality per read"}, xCategories, "Quality score distribution over all sequences");
	}
	
	
	private synchronized void calculateDistribution () {
		
		PhredEncoding encoding = PhredEncoding.getFastQEncodingOffset(lowestChar);
		
		Integer [] rawScores = averageScoreCounts.keySet().toArray(new Integer [0]);
		Arrays.sort(rawScores);
		
		// We'll run from the lowest to the highest
		qualityDistribution = new double [1+(rawScores[rawScores.length-1]-rawScores[0])] ;
		
		xCategories = new int[qualityDistribution.length];
		
		for (int i=0;i<qualityDistribution.length;i++) {
			xCategories[i] = (rawScores[0]+i)-encoding.offset();
			if (averageScoreCounts.containsKey(rawScores[0]+i)) {
				qualityDistribution[i] = averageScoreCounts.get(rawScores[0]+i);
			}
		}

		
		for (int i=0;i<qualityDistribution.length;i++) {
			if (qualityDistribution[i]>maxCount) {
				maxCount = (int)qualityDistribution[i];
				mostFrequentScore = xCategories[i];
			}
		}
				
		calculated = true;
	}

	public void processSequence(Sequence sequence) {
		char [] seq = sequence.getQualityString().toCharArray();
		int averageQuality = 0;
		
		for (int i=0;i<seq.length;i++) {
			if (seq[i] < lowestChar) {
				lowestChar = seq[i];
			}
			averageQuality += seq[i];
		}

		if (seq.length > 0) {
			averageQuality /= seq.length;
		
			if (averageScoreCounts.containsKey(averageQuality)) {
				long currentCount = averageScoreCounts.get(averageQuality);
				currentCount++;
				averageScoreCounts.put(averageQuality, currentCount);
			}
			else {
				averageScoreCounts.put(averageQuality, 1L);
			}
		}
	}
	
	public void reset () {
		averageScoreCounts.clear();
		lowestChar = 126;
		maxCount = 0;
		calculated = false;
	}

	public String description() {
		return "Shows the distribution of average quality scores for whole sequences";
	}

	public String name() {
		return "Per sequence quality scores";
	}

	public boolean raisesError() {
		if (!calculated) calculateDistribution();

		if (mostFrequentScore <=20) return true;
		
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) calculateDistribution();

		if (mostFrequentScore <=27) return true;
		
		return false;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		if (!calculated) calculateDistribution();

		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/per_sequence_quality.png"));

		BufferedImage b = new BufferedImage(800,600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();

		LineGraph lg = new LineGraph(new double [][] {qualityDistribution}, 0d, maxCount, "Mean Sequence Quality (Phred Score)", new String [] {"Average Quality per read"}, xCategories, "Quality score distribution over all sequences");
		lg.paint(g,800,600);

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();

		sb.append("<p><img class=\"indented\" src=\"Images/per_sequence_quality.png\" alt=\"Per Sequence quality graph\"></p>\n");
		
		sb = report.dataDocument();
		sb.append("#Quality\tCount\n");
		for (int i=0;i<xCategories.length;i++) {
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(qualityDistribution[i]);
			sb.append("\n");
		}
	}


	public boolean ignoreFilteredSequences() {
		return true;
	}

}
 public interface QCModule {

	public void processSequence(Sequence sequence);

	public JPanel getResultsPanel();
	
	public String name ();
	
	public String description ();
	
	public void reset ();
	
	public boolean raisesError();
	
	public boolean raisesWarning();
	
	public boolean ignoreFilteredSequences();

	public void makeReport(HTMLReportArchive report) throws IOException;
	
	
}
 public class SequenceLengthDistribution implements QCModule {

	private long [] lengthCounts = new long[0];
	private double [] graphCounts = null;
	private String [] xCategories = new String[0];
	private double max = 0;
	private boolean calculated = false;
	
	public JPanel getResultsPanel() {
	
		if (!calculated) calculateDistribution();
				
		return new LineGraph(new double [][] {graphCounts}, 0d, max, "Sequence Length (bp)",new String [] {"Sequence Length"}, xCategories, "Distribution of sequence lengths over all sequences");
	}
	
	public boolean ignoreFilteredSequences() {
		return true;
	}
	
	private synchronized void calculateDistribution () {
		int maxLen = 0;
		int minLen = -1;
		max = 0;
		
		// Find the min and max lengths
		for (int i=0;i<lengthCounts.length;i++) {
			if (lengthCounts[i]>0) {
				if (minLen < 0) {
					minLen = i;
				}
				maxLen = i;
			}
		}		
		
		// We put one extra category either side of the actual size
		if (minLen>0) minLen--;
		maxLen++;
		
		int [] startAndInterval = getSizeDistribution(minLen, maxLen);
				
		// Work out how many categories we need
		int categories = 0;
		int currentValue = startAndInterval[0];
		while (currentValue<= maxLen) {
			++categories;
			currentValue+= startAndInterval[1];
		}
		
		graphCounts = new double[categories];
		xCategories = new String[categories];
		
		for (int i=0;i<graphCounts.length;i++) {
			
			int minValue = startAndInterval[0]+(startAndInterval[1]*i);
			int maxValue = (startAndInterval[0]+(startAndInterval[1]*(i+1)))-1;

			if (maxValue > maxLen) {
				maxValue = maxLen;
			}
			
			for (int bp=minValue;bp<=maxValue;bp++) {
				if (bp < lengthCounts.length) {
					graphCounts[i] += lengthCounts[bp];
				}
			}

			if (startAndInterval[1] == 1) {
				xCategories[i] = ""+minValue;
			}
			else {
				xCategories[i] = minValue+"-"+maxValue;
			}
			
			if (graphCounts[i] > max) max = graphCounts[i];
		}
		calculated = true;
	}

	public void processSequence(Sequence sequence) {
		int seqLen = sequence.getSequence().length();

		if (seqLen+2 > lengthCounts.length) {
			long [] newLengthCounts = new long[seqLen+2];
			for (int i=0;i<lengthCounts.length;i++) {
				newLengthCounts[i] = lengthCounts[i];
			}
			lengthCounts = newLengthCounts;
		}
		
		++lengthCounts[seqLen];
		
	}
	
	private int [] getSizeDistribution (int min, int max) {
		
		int base = 1;
		
		while (base > (max-min)) {
			base /= 10;
		}
				
		int interval;
		int starting;

		int [] divisions = new int [] {1,2,5};
		
		OUTER: while (true) {
			
			for (int d=0;d<divisions.length;d++) {
				int tester = base * divisions[d];
				if (((max-min) / tester) <= 50) {
					interval = tester;
					break OUTER;
				}
			}
		
			base *=10;
			
		}
		
		// Now we work out the first value to be plotted
		int basicDivision = (int)(min/interval);
				
		int testStart = basicDivision * interval;
				
		starting = testStart;
		
		return new int[] {starting,interval};
		
	}
	
	
	
	public void reset () {
		lengthCounts = new long[0];
	}

	public String description() {
		return "Shows the distribution of sequence length over all sequences";
	}

	public String name() {
		return "Sequence Length Distribution";
	}

	public boolean raisesError() {
		if (!calculated) calculateDistribution();

		if (lengthCounts[0] > 0) {
			return true;
		}
		return false;
	}

	public boolean raisesWarning() {
		if (!calculated) calculateDistribution();

		// Warn if they're not all the same length
		boolean seenLength = false;
		for (int i=0;i<lengthCounts.length;i++) {
			if (lengthCounts[i] > 0) {
				if (seenLength) {
					return true;
				}
				else {
					seenLength = true;
				}
			}
		}
		return false;
	}

	public void makeReport(HTMLReportArchive report) throws IOException {
		if (!calculated) calculateDistribution();

		ZipOutputStream zip = report.zipFile();
		zip.putNextEntry(new ZipEntry(report.folderName()+"/Images/sequence_length_distribution.png"));

		BufferedImage b = new BufferedImage(800,600,BufferedImage.TYPE_INT_RGB);
		Graphics g = b.getGraphics();

		LineGraph lg = new LineGraph(new double [][] {graphCounts}, 0d, max, "Sequence Length (bp)",new String [] {"Sequence Length"}, xCategories, "Distribution of sequence lengths over all sequences");
		lg.paint(g,800,600);

		ImageIO.write((BufferedImage)(b),"PNG",zip);

		StringBuffer sb = report.htmlDocument();

		sb.append("<p><img class=\"indented\" src=\"Images/sequence_length_distribution.png\" alt=\"Sequence length distribution\"></p>\n");
		
		sb = report.dataDocument();
		sb.append("#Length\tCount\n");
		for (int i=0;i<xCategories.length;i++) {
			// Remove any padding we added to make the graph look better
			if ((i==0 || i== xCategories.length-1) && graphCounts[i] == 0) {
				continue;
			}
			sb.append(xCategories[i]);
			sb.append("\t");
			sb.append(graphCounts[i]);
			sb.append("\n");
		}
	}

}
 public class HTMLReportArchive {

	private StringBuffer html = new StringBuffer();
	private StringBuffer data = new StringBuffer();
	private QCModule [] modules;
	private ZipOutputStream zip;
	private SequenceFile sequenceFile;
	private byte [] buffer = new byte[1024];
	private File file;
    private final int read;
    
	public HTMLReportArchive (SequenceFile sequenceFile, QCModule [] modules, File file, int read) throws IOException {
        this.read = read == 0 ? 1 : read;
		this.sequenceFile = sequenceFile;
		this.modules = modules;
		this.file = file;
		zip = new ZipOutputStream(new FileOutputStream(file));
		zip.putNextEntry(new ZipEntry(folderName()+"/"));
		zip.putNextEntry(new ZipEntry(folderName()+"/Icons/"));
		zip.putNextEntry(new ZipEntry(folderName()+"/Images/"));
		startDocument();
		for (int m=0;m<modules.length;m++) {
			html.append("<div class=\"module\"><h2 id=\"M");
			html.append(m);
			html.append("\">");
			
			// Add an icon before the module name
			if (modules[m].raisesError()) {
				html.append("<img src=\"Icons/error.png\" alt=\"[FAIL]\"> ");
			}
			else if (modules[m].raisesWarning()) {
				html.append("<img src=\"Icons/warning.png\" alt=\"[WARN]\"> ");
			}
			else {
				html.append("<img src=\"Icons/tick.png\" alt=\"[OK]\"> ");
			}

			
			
			html.append(modules[m].name());
			data.append(">>");
			data.append(modules[m].name());
			data.append("\t");
			if (modules[m].raisesError()) {
				data.append("fail");
			}
			else if (modules[m].raisesWarning()) {
				data.append("warn");
			}
			else {
				data.append("pass");
			}
			data.append("\n");
			html.append("</h2>\n");
			modules[m].makeReport(this);
			data.append(">>END_MODULE\n");
			
			html.append("</div>\n");
		}
		closeDocument();
		
		zip.putNextEntry(new ZipEntry(folderName()+"/fastqc_report.html"));
		zip.write(html.toString().getBytes());
		zip.closeEntry();
		zip.putNextEntry(new ZipEntry(folderName()+"/fastqc_data.txt"));
		zip.write(data.toString().getBytes());
		zip.closeEntry();
		zip.close();
		
		if (System.getProperty("fastqc.unzip").equals("true")) {
			unzipZipFile(file);
		}
		
		
	}
	
	private void unzipZipFile (File file) throws IOException {
		ZipFile zipFile = new ZipFile(file);
		Enumeration<? extends ZipEntry> entries = zipFile.entries();
		int size;
		byte [] buffer = new byte[1024];
		
		while (entries.hasMoreElements()) {
			ZipEntry entry = entries.nextElement();
			
//			System.out.println("Going to extract '"+entry.getName()+"'");
			
			if (entry.isDirectory()) {
				File dir = new File(file.getParent()+"/"+entry.getName());
				if (dir.exists() && dir.isDirectory()) continue; // Don't need to do anything
				if (dir.exists() && ! dir.isDirectory()) throw new IOException ("File exists with dir name "+dir.getName());
				if (!dir.mkdir()) throw new IOException("Failed to make dir for "+dir.getName());
				continue;
			}

			BufferedInputStream bis = new BufferedInputStream(zipFile.getInputStream(entry));
			BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file.getParent()+"/"+entry.getName()),buffer.length);
			while ((size = bis.read(buffer,0,buffer.length)) != -1) {
				bos.write(buffer,0,size);
			}
			bos.flush();
			bos.close();
			bis.close();
		}
	}

	public StringBuffer htmlDocument () {
		return html;
	}
	
	public StringBuffer dataDocument() {
		return data;
	}
	
	public String folderName () {
		return file.getName().replaceAll(".zip$", "");
	}
	
	public ZipOutputStream zipFile () {
		return zip;
	}
	
	private void startDocument () throws IOException {
		
		// Just put the fastQC version at the start of the text report
		data.append("##FastQC\t");
		data.append(FastQCApplication.VERSION);
		data.append("\n");
		
		// Add in the icon files for pass/fail/warn
		File templatesDir = new File(ClassLoader.getSystemResource("Templates/Icons").getFile().toString().replaceAll("%20", " "));
		String [] names = templatesDir.list();
		for (int n=0;n<names.length;n++) {
			if (names[n].toLowerCase().endsWith(".png") || names[n].toLowerCase().endsWith(".jpg") || names[n].toLowerCase().endsWith(".jpeg")) {
				zip.putNextEntry(new ZipEntry(folderName()+"/Icons/"+names[n]));
				FileInputStream fileIn = new FileInputStream(new File(ClassLoader.getSystemResource("Templates/Icons/"+names[n]).getFile().toString().replaceAll("%20", " ")));
				int len;
				while ((len = fileIn.read(buffer)) > 0) { 
					zip.write(buffer, 0, len); 
				} 
				fileIn.close();
			}
		}		
		

		SimpleDateFormat df = new SimpleDateFormat("EEE d MMM yyyy");
		addTemplate(sequenceFile.name()+" read: "+read,df.format(new Date()));

		html.append("<h2>Summary</h2>\n<ul>\n");
		
		StringBuffer summaryText = new StringBuffer();
		
		for (int m=0;m<modules.length;m++) {
			html.append("<li>");
			html.append("<img src=\"");
			if (modules[m].raisesError()) {
				html.append("Icons/error.png\" alt=\"[FAIL]\"> ");
				summaryText.append("FAIL");
			}
			else if (modules[m].raisesWarning()) {
				html.append("Icons/warning.png\" alt=\"[WARNING]\"> ");				
				summaryText.append("WARN");
			}
			else {
				html.append("Icons/tick.png\" alt=\"[PASS]\"> ");
				summaryText.append("PASS");
			}
			summaryText.append("\t");
			summaryText.append(modules[m].name());
			summaryText.append("\t");
			summaryText.append(sequenceFile.name());
			summaryText.append(System.getProperty("line.separator"));
			
			html.append("<a href=\"#M");
			html.append(m);
			html.append("\">");
			html.append(modules[m].name());
			html.append("</a></li>\n");
			
		}
		html.append("</ul>\n</div>\n<div class=\"main\">\n");

		zip.putNextEntry(new ZipEntry(folderName()+"/summary.txt"));
		zip.write(summaryText.toString().getBytes());

	}
	
	private void closeDocument () {
		
		html.append("</div><div class=\"footer\">Produced by <a href=\"http://www.bioinformatics.bbsrc.ac.uk/projects/fastqc/\">FastQC</a> (version ");
		html.append(FastQCApplication.VERSION);
		html.append(")</div>\n");
		
		html.append("</body></html>");
	}
	
	private void addTemplate (String filename, String date) throws IOException {
		
		BufferedReader br = new BufferedReader(new FileReader(new File(ClassLoader.getSystemResource("Templates/header_template.html").getFile().toString().replaceAll("%20", " "))));
		String line;
		while ((line = br.readLine())!=null) {
			
			line = line.replaceAll("@@FILENAME@@", filename);
			line = line.replaceAll("@@DATE@@", date);
			
			html.append(line);
			html.append("\n");
		}
		
		
		br.close();
	
	}
	
	
	
} public class ResultsPanel extends JPanel implements ListSelectionListener, AnalysisListener{

	private static final ImageIcon ERROR_ICON = new ImageIcon(ClassLoader.getSystemResource("uk/ac/bbsrc/babraham/FastQC/Resources/error.png"));
	private static final ImageIcon WARNING_ICON = new ImageIcon(ClassLoader.getSystemResource("uk/ac/bbsrc/babraham/FastQC/Resources/warning.png"));
	private static final ImageIcon OK_ICON = new ImageIcon(ClassLoader.getSystemResource("uk/ac/bbsrc/babraham/FastQC/Resources/tick.png"));

	
	private QCModule [] modules;
	private JList moduleList;
	private JPanel [] panels;
	private JPanel currentPanel = null;
	private JLabel progressLabel;
	private SequenceFile sequenceFile;
	
	public ResultsPanel (SequenceFile sequenceFile) {
		this.sequenceFile = sequenceFile;
		setLayout(new BorderLayout());
		progressLabel = new JLabel("Waiting to start...",JLabel.CENTER);
		add(progressLabel,BorderLayout.CENTER);
	}

	public void valueChanged(ListSelectionEvent e) {
		int index = moduleList.getSelectedIndex();
		if (index >= 0) {
			remove(currentPanel);
			currentPanel = panels[index]; 
			add(currentPanel,BorderLayout.CENTER);
			validate();
			repaint();
		}
	}
	
	public SequenceFile sequenceFile () {
		return sequenceFile;
	}
	
	public QCModule [] modules () {
		return modules;
	}
	
	private class ModuleRenderer extends DefaultListCellRenderer {

		public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
			if (! (value instanceof QCModule)) {
				return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
			}
			
			QCModule module = (QCModule)value;
			ImageIcon icon = OK_ICON;
			if (module.raisesError()) {
				icon = ERROR_ICON;
			}
			else if (module.raisesWarning()) {
				icon = WARNING_ICON;
			}

			JLabel returnLabel = new JLabel(module.name(),icon,JLabel.LEFT);
			returnLabel.setOpaque(true);
			if (isSelected) {
				returnLabel.setBackground(Color.LIGHT_GRAY);
			}
			else {
				returnLabel.setBackground(Color.WHITE);
			}
			
			return returnLabel;
		}
		
	}

	public void analysisComplete(SequenceFile file, QCModule[] modules) {
		remove(progressLabel);
		this.modules = modules;
		
		panels = new JPanel[modules.length];
		
		for (int m=0;m<modules.length;m++) {
			panels[m] = modules[m].getResultsPanel();
		}

		moduleList = new JList(modules);
		moduleList.setCellRenderer(new ModuleRenderer());
		moduleList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
		moduleList.setSelectedIndex(0);
		moduleList.addListSelectionListener(this);
		
		add(new JScrollPane(moduleList),BorderLayout.WEST);
		
		currentPanel = panels[0];
		add(currentPanel,BorderLayout.CENTER);
		validate();
		
	}

	public void analysisUpdated(SequenceFile file, int sequencesProcessed, int percentComplete) {
		if (percentComplete > 99) {
			progressLabel.setText("Read "+sequencesProcessed+" sequences");			
		}
		else {
			progressLabel.setText("Read "+sequencesProcessed+" sequences ("+percentComplete+"%)");
		}
	}

	public void analysisExceptionReceived(SequenceFile file, Exception e) {
		progressLabel.setText("Failed to process file: "+e.getLocalizedMessage());
	}

	public void analysisStarted(SequenceFile file) {
		progressLabel.setText("Starting analysis...");		
	}
	
	
}
 public class SequenceFactory {

	/**
	 * 
	 * This option is used when multiple files are to be treated as a group to produce
	 * a single output.  This is currently used for groups of files generated by casava
	 * 
	 * @param files
	 * @return
	 * @throws SequenceFormatException
	 * @throws IOException
	 */
	public static SequenceFile getSequenceFile (File [] files, int read) throws SequenceFormatException, IOException {
		
		SequenceFile [] sequenceFiles = new SequenceFile[files.length];
		
		for (int f=0;f<files.length;f++) {
			sequenceFiles[f] = getSequenceFile(files[f], read);
		}
		
		return new SequenceFileGroup(sequenceFiles);
		
	}
	
	public static SequenceFile getSequenceFile (File file, int read) throws SequenceFormatException, IOException {
		
		if (System.getProperty("fastqc.sequence_format") != null) {
			// We're not autodetecting the format, but taking whatever they said
			
			if (System.getProperty("fastqc.sequence_format").equals("bam") || System.getProperty("fastqc.sequence_format").equals("sam")) {
				return new BAMFile(file,false, read);
			}
			else if (System.getProperty("fastqc.sequence_format").equals("bam_mapped") || System.getProperty("fastqc.sequence_format").equals("sam_mapped")) {
				return new BAMFile(file,true, read);
			}
			else if (System.getProperty("fastqc.sequence_format").equals("fastq")) {
				return new FastQFile(file);
			}
//			else if (System.getProperty("fastqc.sequence_format").equals("goby")) {
//				return new GobyFile(file);
//			}
			else {
				throw new SequenceFormatException("Didn't understand format name '"+System.getProperty("fastqc.sequence_format")+"'");
			}
			
		}
		
		
		// Otherwise we just use the extension on the end of the file name to try to determine
		// the type
		if (file.getName().toLowerCase().endsWith(".bam") || file.getName().toLowerCase().endsWith(".sam")) {
			// We default to using all reads
			return new BAMFile(file,false, read);
		}
//		else if (file.getName().toLowerCase().endsWith(".compact-reads") || file.getName().toLowerCase().endsWith(".compact_reads") || file.getName().toLowerCase().endsWith(".goby")) {
//			return new GobyFile(file);
//		}
		else {
			return new FastQFile(file);
		}

		
	}
	
	
}
 public interface SequenceFile {

	public boolean hasNext();
	public Sequence next() throws SequenceFormatException;
	public boolean isColorspace();
	public String name();
	public int getPercentComplete();
	public File getFile();
	
}
 public class SequenceFormatException extends Exception {
	
	public SequenceFormatException (String message) {
		super(message);
	}

}
 public class CasavaBasename {

	/**
	 * This method finds the core name from a CASAVA 1.8 fastq file.  It strips off the
	 * part which indicates that this file is one of a set and returns the base name with
	 * this part removed.
	 * 
	 * If the filename does not conform to standard CASAVA naming then a NameFormatException
	 * is thrown.
	 * 
	 * @param originalName
	 * @return
	 * @throws NameFormatException
	 */
	
	public static String getCasavaBasename (String originalName) throws NameFormatException {
		
		// Find the base name.  We need to remove the 123 numbers from
		// files of the form:
		//
		// anyold_text_123.fastq.gz
		//
		// where the base name will be
		//
		// anyoldtext.fastq.gz
		
		// The file must end with .fastq.gz

		
		if (originalName.endsWith(".fastq.gz")) {
			
			// They must have an _ 13 chars before the end
			if (originalName.substring(originalName.length()-13, originalName.length()-12).equals("_")) {
				
				// They must have numbers for the 3 positions before .fastq
				try {
					Integer.parseInt(originalName.substring(originalName.length()-12, originalName.length()-9));
					
					// If we get here then everything is OK to use the base name from this file
					String baseName = originalName.substring(0,originalName.length()-13)+".fastq.gz";
					return baseName;
				}
				catch (NumberFormatException nfe) {}
			}
		}
		
		throw new NameFormatException();
	}
	
	public static File [][] getCasavaGroups (File [] files) {
		Hashtable<String, Vector<File>> fileBases = new Hashtable<String, Vector<File>>();
		
		for (int f=0;f<files.length;f++) {

			// If a file forms part of a CASAVA group then put it into that
			// group.
			try {
				String baseName = CasavaBasename.getCasavaBasename(files[f].getName());
				if (! fileBases.containsKey(baseName)) {
					fileBases.put(baseName,new Vector<File>());
				}
				fileBases.get(baseName).add(files[f]);

			}
			
			// If the file name doesn't appear to be part of a CASAVA group
			// then add it as a singleton
			catch (NameFormatException nfe) {
				
				System.err.println("File '"+files[f].getName()+"' didn't look like part of a CASAVA group");
				Vector<File> newVector = new Vector<File>();
				newVector.add(files[f]);
				fileBases.put(files[f].getName(), newVector);				
			}
			
		}
		
		String [] baseNames = fileBases.keySet().toArray(new String [0]);
		
		File [][] fileGroups = new File[baseNames.length][];
		
		for (int i=0;i<baseNames.length;i++) {
			fileGroups[i] = fileBases.get(baseNames[i]).toArray(new File[0]);
		}
		
		return fileGroups;		
	}
	
}

import java.awt.BorderLayout;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.UIManager;
import javax.swing.filechooser.FileFilter;
import uk.ac.bbsrc.babraham.FastQC.Analysis.AnalysisRunner;
import uk.ac.bbsrc.babraham.FastQC.Analysis.OfflineRunner;
import uk.ac.bbsrc.babraham.FastQC.Dialogs.WelcomePanel;
import uk.ac.bbsrc.babraham.FastQC.FileFilters.BAMFileFilter;
import uk.ac.bbsrc.babraham.FastQC.FileFilters.CasavaFastQFileFilter;
import uk.ac.bbsrc.babraham.FastQC.FileFilters.FastQFileFilter;
import uk.ac.bbsrc.babraham.FastQC.FileFilters.MappedBAMFileFilter;
import uk.ac.bbsrc.babraham.FastQC.FileFilters.SequenceFileFilter;
import uk.ac.bbsrc.babraham.FastQC.Modules.BasicStats;
import uk.ac.bbsrc.babraham.FastQC.Modules.KmerContent;
import uk.ac.bbsrc.babraham.FastQC.Modules.OverRepresentedSeqs;
import uk.ac.bbsrc.babraham.FastQC.Modules.PerBaseGCContent;
import uk.ac.bbsrc.babraham.FastQC.Modules.NContent;
import uk.ac.bbsrc.babraham.FastQC.Modules.PerBaseQualityScores;
import uk.ac.bbsrc.babraham.FastQC.Modules.PerBaseSequenceContent;
import uk.ac.bbsrc.babraham.FastQC.Modules.PerSequenceGCContent;
import uk.ac.bbsrc.babraham.FastQC.Modules.PerSequenceQualityScores;
import uk.ac.bbsrc.babraham.FastQC.Modules.QCModule;
import uk.ac.bbsrc.babraham.FastQC.Modules.SequenceLengthDistribution;
import uk.ac.bbsrc.babraham.FastQC.Report.HTMLReportArchive;
import uk.ac.bbsrc.babraham.FastQC.Results.ResultsPanel;
import uk.ac.bbsrc.babraham.FastQC.Sequence.SequenceFactory;
import uk.ac.bbsrc.babraham.FastQC.Sequence.SequenceFile;
import uk.ac.bbsrc.babraham.FastQC.Sequence.SequenceFormatException;
import uk.ac.bbsrc.babraham.FastQC.Utilities.CasavaBasename;

/**
 * Copyright Copyright 2010-11 Simon Andrews
 *
 *    This file is part of FastQC.
 *
 *    FastQC is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    FastQC is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with FastQC; if not, write to the Free Software
 *    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package uk.ac.bbsrc.babraham.FastQC;




public class FastQCApplication extends JFrame {	
	
	public static final String VERSION = "0.10.0";
	
	private JTabbedPane fileTabs;
	private WelcomePanel welcomePanel;
	private File lastUsedDir = null;
	
	public FastQCApplication () {
			setTitle("FastQC");
			setIconImage(new ImageIcon(ClassLoader.getSystemResource("uk/ac/bbsrc/babraham/FastQC/Resources/fastqc_icon.png")).getImage());
			setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	//		setSize(1280, 720);
			setSize(800,600);
			setLocationRelativeTo(null);
			
			welcomePanel = new WelcomePanel();
			
			fileTabs = new JTabbedPane(JTabbedPane.TOP);
			setContentPane(welcomePanel);
			
			setJMenuBar(new FastQCMenuBar(this));
			
		}

	public void close () {
		if (fileTabs.getSelectedIndex() >=0) {
			fileTabs.remove(fileTabs.getSelectedIndex());
		}
		if (fileTabs.getTabCount() == 0) {
			setContentPane(welcomePanel);
			validate();
			repaint();
		}
	}
	
	public void closeAll () {
		fileTabs.removeAll();
		setContentPane(welcomePanel);
		validate();
		repaint();
	}
	
	public void openFile () {
		JFileChooser chooser;
		
		if (lastUsedDir == null) {
			chooser = new JFileChooser();
		}
		else {
			chooser = new JFileChooser(lastUsedDir);
		}
		chooser.setMultiSelectionEnabled(true);
		SequenceFileFilter sff = new SequenceFileFilter();
		chooser.addChoosableFileFilter(sff);
		chooser.addChoosableFileFilter(new FastQFileFilter());
		chooser.addChoosableFileFilter(new CasavaFastQFileFilter());
		chooser.addChoosableFileFilter(new BAMFileFilter());
		chooser.addChoosableFileFilter(new MappedBAMFileFilter());
		chooser.setFileFilter(sff);
		int result = chooser.showOpenDialog(this);
		if (result == JFileChooser.CANCEL_OPTION) return;
	
		// See if they forced a file format
		FileFilter chosenFilter = chooser.getFileFilter();
		if (chosenFilter instanceof FastQFileFilter) {
			System.setProperty("fastqc.sequence_format", "fastq");
		}
		if (chosenFilter instanceof CasavaFastQFileFilter) {
			System.setProperty("fastqc.sequence_format", "fastq");
			System.setProperty("fastqc.casava","true");
		}
		else if (chosenFilter instanceof BAMFileFilter) {
			System.setProperty("fastqc.sequence_format", "bam");
		}
		else if (chosenFilter instanceof MappedBAMFileFilter) {
			System.setProperty("fastqc.sequence_format", "bam_mapped");
		}
		
		// If we're still showing the welcome panel switch this out for
		// the file tabs panel
		if (fileTabs.getTabCount() == 0) {
			setContentPane(fileTabs);
			validate();
			repaint();
		}
		
		File [] files = chooser.getSelectedFiles();
		
		File [][] fileGroups;
		
		// See if we need to group together files from a casava group
		if (System.getProperty("fastqc.casava") != null && System.getProperty("fastqc.casava").equals("true")) {
			fileGroups = CasavaBasename.getCasavaGroups(files);
		}
		else {
			fileGroups = new File [files.length][1];
			for (int f=0;f<files.length;f++) {
				fileGroups[f][0] = files[f];
			}
		}

	
		for (int i=0;i<fileGroups.length;i++) {
			File [] filesToProcess = fileGroups[i];
			lastUsedDir = filesToProcess[0].getParentFile();
			SequenceFile sequenceFile;
			
			
			try {
				if (filesToProcess.length > 1) {
					sequenceFile = SequenceFactory.getSequenceFile(filesToProcess, 0);
				}
				else {
					sequenceFile = SequenceFactory.getSequenceFile(filesToProcess[0], 0);
				}
			}
			catch (SequenceFormatException e) {
				JPanel errorPanel = new JPanel();
				errorPanel.setLayout(new BorderLayout());
				errorPanel.add(new JLabel("File format error: "+e.getLocalizedMessage(), JLabel.CENTER),BorderLayout.CENTER);
				fileTabs.addTab(filesToProcess[0].getName(), errorPanel);
				e.printStackTrace();
				continue;
			}
			catch (IOException e) {
				System.err.println("File broken");
				e.printStackTrace();
				JOptionPane.showMessageDialog(this, "Couldn't read file:"+e.getLocalizedMessage(), "Error reading file", JOptionPane.ERROR_MESSAGE);
				continue;
			}
					
			AnalysisRunner runner = new AnalysisRunner(sequenceFile);
			ResultsPanel rp = new ResultsPanel(sequenceFile);
			runner.addAnalysisListener(rp);
			fileTabs.addTab(sequenceFile.name(), rp);
			
			OverRepresentedSeqs os = new OverRepresentedSeqs();
			
			QCModule [] module_list = new QCModule [] {
				new BasicStats(),
				new PerBaseQualityScores(),
				new PerSequenceQualityScores(),
				new PerBaseSequenceContent(),
				new PerBaseGCContent(), 

				new PerSequenceGCContent(),
